<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Tony Bai &#187; GNU</title>
	<atom:link href="http://tonybai.com/tag/gnu/feed/" rel="self" type="application/rss+xml" />
	<link>https://tonybai.com</link>
	<description>一个程序员的心路历程</description>
	<lastBuildDate>Fri, 08 May 2026 23:03:53 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.2.1</generator>
		<item>
		<title>Go 1.25链接器提速、执行文件瘦身：DWARF 5调试信息格式升级终落地</title>
		<link>https://tonybai.com/2025/05/08/go-dwarf5/</link>
		<comments>https://tonybai.com/2025/05/08/go-dwarf5/#comments</comments>
		<pubDate>Thu, 08 May 2025 00:05:53 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[BSS]]></category>
		<category><![CDATA[CD]]></category>
		<category><![CDATA[CI]]></category>
		<category><![CDATA[Clang]]></category>
		<category><![CDATA[Compiler]]></category>
		<category><![CDATA[Debug]]></category>
		<category><![CDATA[delve]]></category>
		<category><![CDATA[DWARF]]></category>
		<category><![CDATA[GCC]]></category>
		<category><![CDATA[GDB]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[GNU]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go1.25]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[issue]]></category>
		<category><![CDATA[link]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[macos]]></category>
		<category><![CDATA[objump]]></category>
		<category><![CDATA[relocation]]></category>
		<category><![CDATA[section]]></category>
		<category><![CDATA[Unix]]></category>
		<category><![CDATA[代码段]]></category>
		<category><![CDATA[数据段]]></category>
		<category><![CDATA[编译]]></category>
		<category><![CDATA[调试]]></category>
		<category><![CDATA[重定位]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=4664</guid>
		<description><![CDATA[本文永久链接 &#8211; https://tonybai.com/2025/05/08/go-dwarf5 大家好，我是Tony Bai。 对于许多Go开发者来说，调试信息的格式可能是一个相对底层的细节。然而，这个细节却对编译速度、最终可执行文件的大小以及调试体验有着深远的影响。经过长达六年的讨论、等待生态成熟和密集的开发工作，Go 语言工具链终于在主干分支（预计将包含在 Go 1.25 中）默认启用了 DWARF version 5 作为其调试信息的标准格式（Issue #26379）。这一看似“幕后”的变更，实则为 Go 开发者带来了切实的链接速度提升和可执行文件体积的优化。在这篇文章中，我们就来对DWARF5落地Go这件事儿做一个简单的解读。 为何需要升级到 DWARF 5？旧格式的痛点 DWARF (Debugging With Attributed Record Formats) 是类 Unix 系统上广泛使用的调试信息标准。Go 之前使用的 DWARF 版本（主要是 v2 和 v4）虽然成熟，但在现代软件开发实践中暴露出一些不足： 大量的重定位 (Relocations): 旧版 DWARF 格式通常包含大量需要链接器处理的地址重定位信息。根据 2018 年的初步分析（by aclements），在当时的 go 二进制文件中，高达 49% 的重定位条目都源于 DWARF 数据。这显著增加了链接器的工作负担，拖慢了构建速度，尤其是对于大型项目。 冗长的位置和范围列表 (Location/Range Lists): 用于描述变量生命周期和代码范围的 .debug_loc 和 [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/2025/go-dwarf5-1.jpg" alt="" /></p>
<p><a href="https://tonybai.com/2025/05/08/go-dwarf5">本文永久链接</a> &#8211; https://tonybai.com/2025/05/08/go-dwarf5</p>
<p>大家好，我是Tony Bai。</p>
<p>对于许多Go开发者来说，调试信息的格式可能是一个相对底层的细节。然而，这个细节却对编译速度、最终可执行文件的大小以及调试体验有着深远的影响。经过长达六年的讨论、等待生态成熟和密集的开发工作，Go 语言工具链终于在主干分支（预计将包含在 Go 1.25 中）默认启用了 <strong>DWARF version 5</strong> 作为其调试信息的标准格式（<a href="https://github.com/golang/go/issues/26379">Issue #26379</a>）。这一看似“幕后”的变更，实则为 Go 开发者带来了切实的<strong>链接速度提升</strong>和<strong>可执行文件体积的优化</strong>。在这篇文章中，我们就来对DWARF5落地Go这件事儿做一个简单的解读。</p>
<h2>为何需要升级到 DWARF 5？旧格式的痛点</h2>
<p>DWARF (Debugging With Attributed Record Formats) 是类 Unix 系统上广泛使用的调试信息标准。Go 之前使用的 DWARF 版本（主要是 v2 和 v4）虽然成熟，但在现代软件开发实践中暴露出一些不足：</p>
<ol>
<li><strong>大量的重定位 (Relocations):</strong> 旧版 DWARF 格式通常包含大量需要链接器处理的地址重定位信息。根据 2018 年的初步分析（by aclements），在当时的 go 二进制文件中，高达 <strong>49%</strong> 的重定位条目都源于 DWARF 数据。这显著增加了链接器的工作负担，拖慢了构建速度，尤其是对于大型项目。</li>
<li><strong>冗长的位置和范围列表 (Location/Range Lists):</strong> 用于描述变量生命周期和代码范围的 .debug_loc 和 .debug_ranges 等section的数据在旧格式下可能非常庞大。即便经过压缩，它们也能占到可执行文件大小的相当一部分（例如，当时 go 二进制的 12MiB 中占 6%）。</li>
<li><strong>缺乏官方 Go 语言代码:</strong> 虽然不影响功能，但 DWARF 5 正式为 Go 语言分配了官方的语言代码 (DW_LANG_Go)。</li>
</ol>
<p>DWARF 5 标准针对这些痛点进行了改进，其关键优势在于：</p>
<ul>
<li><strong>位置无关表示 (Position-Independent Representations):</strong> DWARF 5 引入了如 .debug_addr, .debug_rnglists, .debug_loclists 等新 Section 格式，它们的设计能大幅减少甚至消除对重定位的需求，从而减轻链接器负担。</li>
<li><strong>更紧凑的列表格式:</strong> 新的列表格式 (.debug_rnglists, .debug_loclists) 比旧的 (.debug_ranges, .debug_loc) 更为紧凑，有助于减小调试信息的大小。</li>
</ul>
<h2>从提案到落地：漫长的等待与集中的开发</h2>
<p>尽管 DWARF 5 的优势显而易见，但 Go 社区在 2018 年提出该想法时（by aclements），整个开发工具生态（如调试器 LLDB、macOS 的链接器和 dsymutil 工具等）对其支持尚不完善。因此，该提案被暂时搁置，等待时机成熟。</p>
<p>近年来，随着主流工具链（GCC 7.1+, GDB 8.0+, Clang 14+）纷纷将 DWARF 5 作为默认选项，生态环境逐渐成熟。Go 团队成员 <strong>Than McIntosh</strong> 承担了将 Go 工具链迁移到 DWARF 5 的主要开发工作。这涉及对编译器 (cmd/compile) 和链接器 (cmd/link) 的大量修改，引入了新的 GOEXPERIMENT=dwarf5 实验开关进行测试，并提交了一系列相关的变更集 (CLs)，包括：</p>
<ul>
<li>添加 DWARF 5 相关常量和 relocation 类型定义。</li>
<li>实现对 .debug_addr, .debug_rnglists, .debug_loclists section 的生成和支持。</li>
<li>更新 DWARF 5 的行号表 (line table) 支持。</li>
<li>适配 x/debug/dwtest 和 internal/gocore 等内部库。</li>
<li>协调 Delve 调试器对 DWARF 5 的支持。</li>
</ul>
<h2>成果显著：链接速度提升与体积优化</h2>
<p>经过广泛的测试和 compilebench 基准评估，启用 DWARF 5 带来了可观的性能收益：</p>
<ul>
<li><strong>链接速度显著提升:</strong> ExternalLinkCompiler 基准测试显示链接时间减少了 <strong>约 14%</strong>。这主要得益于 DWARF 5 减少了链接器需要处理的重定位数量。</li>
<li><strong>可执行文件体积减小:</strong> HelloSize 和 CmdGoSize 基准显示最终可执行文件大小平均减小了 <strong>约 3%</strong>。这归功于 DWARF 5 更紧凑的列表格式。</li>
<li><strong>编译时间略有改善:</strong> 整体编译时间 (geomean) 也有约 <strong>1.9%</strong> 的小幅提升。</li>
</ul>
<p>虽然对代码段 (.text)、数据段 (.data)、BSS 段的大小几乎没有影响，但链接耗时和最终文件大小的优化对于大型项目和 CI/CD 流程来说意义重大。</p>
<h2>挑战与妥协：并非所有平台一步到位</h2>
<p>在推进 DWARF 5 的过程中，也遇到了一些平台兼容性问题，导致 Go 团队采取了审慎的策略：</p>
<ol>
<li><strong>macOS dsymutil 限制:</strong> 旧版本的 macOS Xcode 自带的 dsymutil 工具（用于处理和分离 DWARF 信息）不支持 DWARF 5 新引入的 .debug_rnglists 和 .debug_loclists section。这会导致在使用<strong>外部链接 (external linking)</strong> 构建 CGO 程序时，Go 代码的调试信息丢失。虽然 LLVM 17 (对应 Xcode 16+) 已修复此问题，但考虑到仍有大量开发者使用旧版 Xcode（官方支持最低到 Xcode 14），Go 团队决定<strong>在 macOS 和 iOS 平台上进行外部链接时，暂时回退到 DWARF 4</strong>。未来当最低支持的 Xcode 版本兼容 DWARF 5 后，有望统一。</li>
<li><strong>AIX 平台限制:</strong> AIX 使用的 XCOFF 文件格式本身不支持 DWARF 5 所需的 Section 类型。因此，<strong>AIX 平台将继续使用 DWARF 4</strong> (GOEXPERIMENT=nodwarf5 默认开启)。</li>
<li><strong>GNU objdump 兼容性:</strong> objdump 工具在解析 Go 生成的 monolithic .debug_addr section 时会打印警告（因为它期望每个编译单元都有一个 header，而 Go 链接器只生成一个）。这被认为是一个 objdump 的小问题（已提议向上游提交修复），不影响实际功能，因此 Go 团队决定继续采用 monolithic 方式。</li>
</ol>
<h2>对开发者的影响与总结</h2>
<p>对于大多数 Go 开发者而言，这项变更将在 Go 1.25 及以后版本中<strong>默认生效</strong>（除了上述 macOS 外部链接和 AIX 平台）。你将自动享受到<strong>更快的链接速度</strong>和<strong>略小的可执行文件</strong>。</p>
<ul>
<li><strong>调试体验:</strong> 虽然 DWARF 5 本身设计更优，但对日常使用 Delve 等调试器的直接体验影响可能不明显，主要好处体现在工具链效率和文件大小上。</li>
<li><strong>注意事项:</strong> 如果你在 macOS 上进行 CGO 开发并使用外部链接，或者面向 AIX 平台，需要了解调试信息格式仍将是 DWARF 4。</li>
</ul>
<p>总而言之，Go 工具链采纳 DWARF 5 是一个重要的里程碑。它不仅解决了旧格式的一些固有问题，提升了构建效率，也是 Go 语言紧跟底层技术标准发展、持续优化开发者体验的重要一步。这项历时多年的工作最终落地，体现了 Go 社区在推动技术演进方面的耐心和决心。</p>
<h2>参考资料</h2>
<ul>
<li><a href="https://github.com/golang/go/issues/26379">cmd/compile: consider using DWARF 5</a> &#8211; https://github.com/golang/go/issues/26379</li>
<li><a href="https://dwarfstd.org/dwarf5std.html">DWARF Version 5</a> &#8211; https://dwarfstd.org/dwarf5std.html</li>
</ul>
<hr />
<p><strong>聊聊你的编译构建体验</strong></p>
<p>Go 1.25 工具链的这项 DWARF 5 升级，虽然“藏”在幕后，但实实在在地为我们带来了链接速度和文件大小的优化。<strong>你在日常的 Go 项目开发中，是否也曾被编译链接速度或可执行文件体积困扰过？</strong> 你对 Go 工具链在这些方面的持续改进有什么期待或建议吗？或者，你是否了解其他能有效优化构建体验的技巧？</p>
<p><strong>欢迎在评论区分享你的经验、痛点与期待！</strong> 让我们共同见证 Go 工具链的进步。</p>
<p><strong>想深入探索Go的编译、链接与底层奥秘？</strong></p>
<p>如果你对 Go 工具链如何工作、编译优化、链接器原理，乃至像 DWARF 这样的底层细节充满兴趣，希望系统性地构建对 Go 语言“从源码到可执行文件”全链路的深刻理解&#8230;</p>
<p>那么，我的 <strong>「Go &amp; AI 精进营」知识星球</strong> 正是为你打造的深度学习平台！这里有【Go原理课】带你解密语言核心机制，【Go进阶课】助你掌握高级技巧，更有【Go避坑课】让你少走弯路。我会亲自为你解答各种疑难问题，你还可以与众多热爱钻研的Gopher们一同交流，探索Go的更多可能，包括它在AI等前沿领域的应用。</p>
<p><strong>扫码加入，与我们一同潜入Go的底层世界，成为更懂Go的开发者！</strong></p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-and-ai-tribe-zsxq-small-card.jpg" alt="img{512x368}" /></p>
<hr />
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。</p>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p style='text-align:left'>&copy; 2025, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2025/05/08/go-dwarf5/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go开发命令行程序指南</title>
		<link>https://tonybai.com/2023/03/25/the-guide-of-developing-cli-program-in-go/</link>
		<comments>https://tonybai.com/2023/03/25/the-guide-of-developing-cli-program-in-go/#comments</comments>
		<pubDate>Fri, 24 Mar 2023 22:35:58 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[AIGC]]></category>
		<category><![CDATA[argument]]></category>
		<category><![CDATA[benchmark]]></category>
		<category><![CDATA[brew]]></category>
		<category><![CDATA[ChatGPT]]></category>
		<category><![CDATA[cli]]></category>
		<category><![CDATA[clig.dev]]></category>
		<category><![CDATA[cobra]]></category>
		<category><![CDATA[Context]]></category>
		<category><![CDATA[coverage]]></category>
		<category><![CDATA[curl]]></category>
		<category><![CDATA[defer]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[errors]]></category>
		<category><![CDATA[flag]]></category>
		<category><![CDATA[fmt]]></category>
		<category><![CDATA[Git]]></category>
		<category><![CDATA[GNU]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go-module]]></category>
		<category><![CDATA[go.mod]]></category>
		<category><![CDATA[go1.11]]></category>
		<category><![CDATA[go1.20]]></category>
		<category><![CDATA[gobuild]]></category>
		<category><![CDATA[goinstall]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[GOPATH]]></category>
		<category><![CDATA[goreleaser]]></category>
		<category><![CDATA[GPT-4]]></category>
		<category><![CDATA[grep]]></category>
		<category><![CDATA[help]]></category>
		<category><![CDATA[Interface]]></category>
		<category><![CDATA[kingpin]]></category>
		<category><![CDATA[kubectl]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[log]]></category>
		<category><![CDATA[Mock]]></category>
		<category><![CDATA[option]]></category>
		<category><![CDATA[pkg]]></category>
		<category><![CDATA[POSIX]]></category>
		<category><![CDATA[README]]></category>
		<category><![CDATA[Signal]]></category>
		<category><![CDATA[subcommand]]></category>
		<category><![CDATA[subtest]]></category>
		<category><![CDATA[tar]]></category>
		<category><![CDATA[testify]]></category>
		<category><![CDATA[Testing]]></category>
		<category><![CDATA[Unix]]></category>
		<category><![CDATA[usage]]></category>
		<category><![CDATA[主命令]]></category>
		<category><![CDATA[优雅退出]]></category>
		<category><![CDATA[依赖管理]]></category>
		<category><![CDATA[参数]]></category>
		<category><![CDATA[命令行程序]]></category>
		<category><![CDATA[子命令]]></category>
		<category><![CDATA[标志]]></category>
		<category><![CDATA[表驱动测试]]></category>
		<category><![CDATA[选项]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=3837</guid>
		<description><![CDATA[注：上面篇首配图的底图由百度文心一格生成。 本文永久链接 &#8211; https://tonybai.com/2023/03/25/the-guide-of-developing-cli-program-in-go 近期在Twitter上看到一个名为“Command Line Interface Guidelines”的站点，这个站点汇聚了帮助大家编写出更好命令行程序的哲学与指南。这份指南基于传统的Unix编程原则，又结合现代的情况进行了“与时俱进”的更新。之前我还真未就如何编写命令行交互程序做系统的梳理，在这篇文章中，我们就来结合clig这份指南，(可能不会全面覆盖)整理出一份使用Go语言编写CLI程序的指南，供大家参考。 一. 命令行程序简介 命令行接口（Command Line Interface, 简称CLI）程序是一种允许用户使用文本命令和参数与计算机系统互动的软件。开发人员编写CLI程序通常用在自动化脚本、数据处理、系统管理和其他需要低级控制和灵活性的任务上。命令行程序也是Linux/Unix管理员以及后端开发人员的最爱。 2022年Q2 Go官方用户调查结果显示(如下图)：在使用Go开发的程序类别上，CLI类程序排行第二，得票率60%。 之所以这样，得益于Go语言为CLI开发提供的诸多便利，比如： Go语法简单而富有表现力； Go拥有一个强大的标准库，并内置的并发支持； Go拥有几乎最好的跨平台兼容性和快速的编译速度； Go还有一个丰富的第三方软件包和工具的生态系统。 这些都让开发者使用Go创建强大和用户友好的CLI程序变得容易。 容易归容易，但要用Go编写出优秀的CLI程序，我们还需要遵循一些原则，获得一些关于Go CLI程序开发的最佳实践和惯例。这些原则和惯例涉及交互界面设计、错误处理、文档、测试和发布等主题。此外，借助于一些流行的Go CLI程序开发库和框架，比如：cobra、Kingpin和Goreleaser等，我们可以又好又快地完成CLI程序的开发。在本文结束时，你将学会如何创建一个易于使用、可靠和可维护的Go CLI程序，你还将获得一些关于CLI开发的最佳实践和惯例的见解。 二. 建立Go开发环境 如果你读过《十分钟入门Go语言》或订阅学习过我的极客时间《Go语言第一课》专栏，你大可忽略这一节的内容。 在我们开始编写Go CLI程序之前，我们需要确保我们的系统中已经安装和配置了必要的Go工具和依赖。在本节中，我们将向你展示如何安装Go和设置你的工作空间，如何使用go mod进行依赖管理，以及如何使用go build和go install来编译和安装你的程序。 1. 安装Go 要在你的系统上安装Go，你可以遵循你所用操作系统的官方安装说明。你也可以使用软件包管理器，如homebrew（用于macOS）、chocolatey（用于Windows）或snap/apt（用于Linux）来更容易地安装Go。 一旦你安装了Go，你可以通过在终端运行以下命令来验证它是否可以正常工作。 $go version 如果安装成功，go version这个命令应该会打印出你所安装的Go的版本。比如说： go version go1.20 darwin/amd64 2. 设置你的工作区(workspace) Go以前有一个惯例，即在工作区目录中(\$GOPATH)组织你的代码和依赖关系。默认工作空间目录位于$HOME/go，但你可以通过设置GOPATH环境变量来改变它的路径。工作区目录包含三个子目录：src、pkg和bin。src目录包含了你的源代码文件和目录。pkg目录包含被你的代码导入的已编译好的包。bin目录包含由你的代码生成的可执行二进制文件。 Go 1.11引入Go module后，这种在\$GOPATH下组织代码和寻找依赖关系的要求被彻底取消。在这篇文章中，我依旧按照我的习惯在$HOME/go/src下放置我的代码示例。 为了给我们的CLI程序创建一个新的项目目录，我们可以在终端运行以下命令： $mkdir -p [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/the-guide-of-developing-cli-program-in-go-1.png" alt="" /></p>
<blockquote>
<p>注：上面篇首配图的底图由百度<a href="https://yige.baidu.com">文心一格</a>生成。</p>
</blockquote>
<p><a href="https://tonybai.com/2023/03/25/the-guide-of-developing-cli-program-in-go">本文永久链接</a> &#8211; https://tonybai.com/2023/03/25/the-guide-of-developing-cli-program-in-go</p>
<p>近期在Twitter上看到一个名为<a href="https://clig.dev/">“Command Line Interface Guidelines”的站点</a>，这个站点汇聚了帮助大家编写出更好命令行程序的哲学与指南。这份指南<a href="https://book.douban.com/subject/1467587/">基于传统的Unix编程原则</a>，又结合现代的情况进行了“与时俱进”的更新。之前我还真未就如何编写命令行交互程序做系统的梳理，在这篇文章中，我们就来结合<a href="https://clig.dev/">clig这份指南</a>，(可能不会全面覆盖)整理出一份使用Go语言编写CLI程序的指南，供大家参考。</p>
<h2>一. 命令行程序简介</h2>
<p>命令行接口（Command Line Interface, 简称CLI）程序是一种允许用户使用文本命令和参数与计算机系统互动的软件。开发人员编写CLI程序通常用在自动化脚本、数据处理、系统管理和其他需要低级控制和灵活性的任务上。<strong>命令行程序也是Linux/Unix管理员以及后端开发人员的最爱</strong>。</p>
<p><a href="https://go.dev/blog/survey2022-q2-results">2022年Q2 Go官方用户调查结果</a>显示(如下图)：在使用Go开发的程序类别上，CLI类程序排行第二，得票率60%。</p>
<p><img src="https://tonybai.com/wp-content/uploads/the-guide-of-developing-cli-program-in-go-2.png" alt="" /></p>
<p>之所以这样，得益于Go语言为CLI开发提供的诸多便利，比如：</p>
<ul>
<li>Go语法简单而富有表现力；</li>
<li>Go拥有一个强大的标准库，并内置的并发支持；</li>
<li>Go拥有几乎最好的跨平台兼容性和快速的编译速度；</li>
<li>Go还有一个丰富的第三方软件包和工具的生态系统。</li>
</ul>
<p>这些都让开发者使用Go创建强大和用户友好的CLI程序变得容易。</p>
<p>容易归容易，但要用Go编写出优秀的CLI程序，我们还需要遵循一些原则，获得一些关于Go CLI程序开发的最佳实践和惯例。这些原则和惯例涉及交互界面设计、错误处理、文档、测试和发布等主题。此外，借助于一些流行的Go CLI程序开发库和框架，比如：<a href="https://github.com/spf13/cobra">cobra</a>、<a href="https://github.com/alecthomas/kingpin">Kingpin</a>和<a href="https://github.com/goreleaser/goreleaser">Goreleaser</a>等，我们可以又好又快地完成CLI程序的开发。在本文结束时，你将学会如何创建一个易于使用、可靠和可维护的Go CLI程序，你还将获得一些关于CLI开发的最佳实践和惯例的见解。</p>
<h2>二. 建立Go开发环境</h2>
<blockquote>
<p>如果你读过<a href="https://tonybai.com/2023/02/23/learn-go-in-10-min">《十分钟入门Go语言》</a>或订阅学习过我的极客时间<a href="http://gk.link/a/10AVZ">《Go语言第一课》专栏</a>，你大可忽略这一节的内容。</p>
</blockquote>
<p>在我们开始编写Go CLI程序之前，我们需要确保我们的系统中已经安装和配置了必要的Go工具和依赖。在本节中，我们将向你展示如何安装Go和设置你的工作空间，如何<a href="https://tonybai.com/2022/03/12/dependency-hell-in-go/">使用go mod进行依赖管理</a>，以及如何使用go build和go install来编译和安装你的程序。</p>
<h3>1. 安装Go</h3>
<p>要在你的系统上安装Go，你可以遵循你所用操作系统的官方安装说明。你也可以使用软件包管理器，如<a href="https://brew.sh">homebrew</a>（用于macOS）、chocolatey（用于Windows）或snap/apt（用于Linux）来更容易地安装Go。</p>
<p>一旦你安装了Go，你可以通过在终端运行以下命令来验证它是否可以正常工作。</p>
<pre><code>$go version
</code></pre>
<p>如果安装成功，go version这个命令应该会打印出你所安装的Go的版本。比如说：</p>
<pre><code>go version go1.20 darwin/amd64
</code></pre>
<h3>2. 设置你的工作区(workspace)</h3>
<p>Go以前有一个惯例，即在工作区目录中(\$GOPATH)组织你的代码和依赖关系。默认工作空间目录位于$HOME/go，但你可以通过设置GOPATH环境变量来改变它的路径。工作区目录包含三个子目录：src、pkg和bin。src目录包含了你的源代码文件和目录。pkg目录包含被你的代码导入的已编译好的包。bin目录包含由你的代码生成的可执行二进制文件。</p>
<p><a href="https://tonybai.com/2018/11/19/some-changes-in-go-1-11">Go 1.11引入Go module</a>后，这种在\$GOPATH下组织代码和寻找依赖关系的要求被彻底取消。在这篇文章中，我依旧按照我的习惯在$HOME/go/src下放置我的代码示例。</p>
<p>为了给我们的CLI程序创建一个新的项目目录，我们可以在终端运行以下命令：</p>
<pre><code>$mkdir -p $HOME/go/src/github.com/your-username/your-li-program
$cd $HOME/go/src/github.com/your-username/your-cli-program
</code></pre>
<p>注意，我们的项目目录名使用的是github的URL格式。这在Go项目中是一种常见的做法，因为它使得使用go get导入和管理依赖关系更加容易。go module成为构建标准后，这种对项目目录名的要求已经取消，但很多Gopher依旧保留了这种作法。</p>
<h3>3. 使用go mod进行依赖管理</h3>
<p>1.11版本后Go推荐开发者使用module来管理包的依赖关系。一个module是共享一个共同版本号和导入路径前缀的相关包的集合。一个module是由一个叫做go.mod的文件定义的，它指定了模块的名称、版本和依赖关系。</p>
<p>为了给我们的CLI程序创建一个新的module，我们可以在我们的项目目录下运行以下命令。</p>
<pre><code>$go mod init github.com/your-username/your-cli-program
</code></pre>
<p>这将创建一个名为go.mod的文件，内容如下。</p>
<pre><code>module github.com/your-username/your-cli-program

go 1.20
</code></pre>
<p>第一行指定了我们的module名称，这与我们的项目目录名称相匹配。第二行指定了构建我们的module所需的Go的最低版本。</p>
<p>为了给我们的模块添加依赖项，我们可以使用go get命令，加上我们想使用的软件包的导入路径和可选的版本标签。例如，如果我们想使用<a href="https://github.com/spf13/cobra">cobra</a>作为我们的CLI框架，我们可以运行如下命令：</p>
<pre><code>$go get github.com/spf13/cobra@v1.3.0
</code></pre>
<p>go get将从github下载cobra，并在我们的go.mod文件中把它作为一个依赖项添加进去。它还将创建或更新一个名为go.sum的文件，记录所有下载的module的校验和，以供后续验证使用。</p>
<p>我们还可以使用其他命令，如go list、go mod tidy、go mod graph等，以更方便地检查和管理我们的依赖关系。</p>
<h3>4. 使用go build和go install来编译和安装你的程序</h3>
<p>Go有两个命令允许你编译和安装你的程序：go build和go install。这两个命令都以一个或多个包名或导入路径作为参数，并从中产生可执行的二进制文件。</p>
<p>它们之间的主要区别在于它们将生成的二进制文件存储在哪里。</p>
<ul>
<li>go build将它们存储在当前工作目录中。</li>
<li>go install将它们存储在\$GOPATH/bin或\$GOBIN（如果设置了）。</li>
</ul>
<p>例如，如果我们想把CLI程序的main包（应该位于github.com/your-username/your-cli-program/cmd/your-cli-program）编译成一个可执行的二进制文件，称为your-cli-program，我们可以运行下面命令：</p>
<pre><code>$go build github.com/your-username/your-cli-program/cmd/your-cli-program
</code></pre>
<p>或</p>
<pre><code>$go install github.com/your-username/your-cli-program/cmd/your-cli-program@latest
</code></pre>
<h2>三. 设计用户接口(interface)</h2>
<p>要编写出一个好的CLI程序，最重要的环节之一是<strong><a href="https://clig.dev/#human-first-design">设计一个用户友好的接口</a></strong>。好的命令行用户接口应该是<strong>一致的、直观的和富有表现力的</strong>。在本节中，我将说明如何为命令行程序命名和选择命令结构(command structure)，如何使用标志(flag)、参数(argument)、子命令(subcommand)和选项(option)作为输入参数，如何使用cobra或Kingpin等来解析和验证用户输入，以及如何遵循POSIX惯例和GNU扩展的CLI语法。</p>
<h3>1. 命令行程序命名和命令结构选择</h3>
<p>你的CLI程序的名字应该是<strong><a href="https://clig.dev/#naming">简短、易记、描述性的和易输入的</a></strong>。它应该避免与目标平台中现有的命令或关键字发生冲突。例如，如果你正在编写一个在不同格式之间转换图像的程序，你可以把它命名为imgconv、imago、picto等，但不能叫image、convert或format。</p>
<p>你的CLI程序的命令结构应该反映你想提供给用户的主要功能特性。你可以选择使用下面命令结构模式中的一种：</p>
<ul>
<li>一个带有多个标志(flag)和参数(argument)的单一命令（例如：curl、tar、grep等)</li>
<li>带有多个子命令(subcommand)的单一命令（例如：git、docker、kubectl等)</li>
<li>具有共同前缀的多个命令（例如：aws s3、gcloud compute、az vm等)</li>
</ul>
<p>命令结构模式的选择取决于你的程序的复杂性和使用范围，一般来说：</p>
<ul>
<li>如果你的程序只有一个主要功能或操作模式(operation mode)，你可以使用带有多个标志和参数的单一命令。</li>
<li>如果你的程序有多个相关但又不同的功能或操作模式，你可以使用一个带有多个子命令的单一命令。</li>
<li>如果你的程序有多个不相关或独立的功能或操作模式，你可以使用具有共同前缀的多个命令。</li>
</ul>
<p>例如，如果你正在编写一个对文件进行各种操作的程序（如复制、移动、删除），你可以任选下面命令结构模式中的一种：</p>
<ul>
<li>带有多个标志和参数的单一命令（例如，fileop -c src dst -m src dst -d src)</li>
<li>带有多个子命令的单个命令（例如，fileop copy src dst, fileop move src dst, fileop delete src)</li>
</ul>
<h3>2. 使用标志、参数、子命令和选项</h3>
<p><strong>标志(flag)</strong>是以一个或多个(通常是2个)中划线（-）开头的输入参数，它可以修改CLI程序的行为或输出。例如：</p>
<pre><code>$curl -s -o output.txt https://example.com
</code></pre>
<p>在这个例子中：</p>
<ul>
<li>“-s”是一个让curl沉默的标志，即不输出执行日志到控制台；</li>
<li>“-o”是另一个标志，用于指定输出文件的名称</li>
<li>“output.txt”则是一个参数，是为“-o”标志提供的值。</li>
</ul>
<p><strong>参数(argument)</strong>是不以中划线（-）开头的输入参数，为你的CLI程序提供额外的信息或数据。例如：</p>
<pre><code>$tar xvf archive.tar.gz
</code></pre>
<p>我们看在这个例子中：</p>
<ul>
<li>x是一个指定提取模式的参数</li>
<li>v是一个参数，指定的是输出内容的详细(verbose)程度</li>
<li>f是另一个参数，用于指定采用的是文件模式，即将压缩结果输出到一个文件或从一个压缩文件读取数据</li>
<li>archive.tar.gz是一个参数，提供文件名。</li>
</ul>
<p><strong>子命令(subcommand)</strong>是输入参数，作为主命令下的辅助命令。它们通常有自己的一组标志和参数。比如下面例子：</p>
<pre><code>$git commit -m "Initial commit"
</code></pre>
<p>我们看在这个例子中：</p>
<ul>
<li>git是主命令(primary command)</li>
<li>commit是一个子命令，用于从staged的修改中创建一个新的提交(commit)</li>
<li>“-m”是commit子命令的一个标志，用于指定提交信息</li>
<li>“Initial commit”是commit子命令的一个参数，为”-m”标志提供值。</li>
</ul>
<p><strong>选项(option)</strong>是输入参数，它可以使用等号（=）将标志和参数合并为一个参数。例如:</p>
<pre><code>$docker run --name=my-container ubuntu:latest
</code></pre>
<p>我们看在这个例子中“&#8211;name=my-container”是一个选项，它将容器的名称设为my-container。该选项前面的部分“&#8211;name”是一个标志，后面的部分“my-container”是参数。</p>
<h3>3. 使用cobra包等来解析和验证用户输入的信息</h3>
<p>如果手工来解析和验证用户输入的信息，既繁琐又容易出错。幸运的是，有许多库和框架可以帮助你在Go中解析和验证用户输入。其中最流行的是<a href="https://github.com/spf13/cobra">cobra</a>。</p>
<p>cobra是一个Go包，它提供了简单的接口来创建强大的CLI程序。它支持子命令、标志、参数、选项、环境变量和配置文件。它还能很好地与其他库集成，比如：<a href="https://tonybai.com/2022/09/20/use-viper-to-do-merge-of-yml-configuration-files/">viper</a>（用于配置管理）、<a href="https://github.com/spf13/pflag">pflag</a>（用于POSIX/GNU风格的标志）和<a href="https://github.com/docopt/docopt.go">Docopt</a>（用于生成文档）。</p>
<p>另一个不那么流行但却提供了一种声明式的方法来创建优雅的CLI程序的包是<a href="https://github.com/alecthomas/kingpin">Kingpin</a>，它支持标志、参数、选项、环境变量和配置文件。它还具有自动帮助生成、命令完成、错误处理和类型转换等功能。</p>
<p>cobra和Kingpin在其官方网站上都有大量的文档和例子，你可以根据你的偏好和需要选择任选其一。</p>
<h3>4. 遵循POSIX惯例和GNU扩展的CLI语法</h3>
<p><a href="http://get.posixcertified.ieee.org">POSIX（Portable Operating System Interface）</a>是一套标准，定义了软件应该如何与操作系统进行交互。其中一个标准定义了CLI程序的语法和语义。GNU（GNU&#8217;s Not Unix）是一个旨在创建一个与UNIX兼容的自由软件操作系统的项目。GNU下的一个子项目是<a href="https://www.gnu.org/software/coreutils/">GNU Coreutils</a>，它提供了许多常见的CLI程序，如ls、cp、mv等。</p>
<p>POSIX和GNU都为CLI语法建立了一些约定和扩展，许多CLI程序都采用了这些约定与扩展。下面列举了这些约定和扩展中的一些主要内容：</p>
<ul>
<li>单字母标志(single-letter flag)以一个中划线（-）开始，可以组合在一起（例如：-a -b -c 或 -abc )</li>
<li>长标志(long flag)以两个中划线（&#8211;）开头，但不能组合在一起（例如：&#8211;all、&#8211;backup、&#8211;color )</li>
<li>选项使用等号(=)来分隔标志名和参数值(例如：&#8211;name=my-container )</li>
<li>参数跟在标志或选项之后，没有任何分隔符（例如：curl -o output.txt https://example.com ）。</li>
<li>子命令跟在主命令之后，没有任何分隔符（例如：git commit -m “Initial commit” )</li>
<li>一个双中划线（&#8211;）表示标志或选项的结束和参数的开始（例如：rm &#8212; -f 表示要删除“-f”这个文件，由于双破折线的存在，这里的“-f”不再是标志)</li>
</ul>
<p>遵循这些约定和扩展可以使你的CLI程序更加一致、直观，并与其他CLI程序兼容。然而，它们并不是强制性的，如果你有充分的理由，你也大可不必完全遵守它们。例如，一些CLI程序使用斜线（/）而不是中划线（-）表示标志（例如， robocopy /S /E src dst ）。</p>
<h2>四. 处理错误和信号</h2>
<p>编写好的CLI程序的一个重要环节就是<strong><a href="https://clig.dev/#errors">优雅地处理错误和信号</a></strong>。</p>
<p>错误是指你的程序由于某些内部或外部因素而无法执行其预定功能的情况。信号是由操作系统或其他进程向你的程序发送的事件，以通知它一些变化或请求。在这一节中，我将说明一下如何使用log、fmt和errors包进行日志输出和错误处理，如何使用os.Exit和defer语句进行优雅的终止，如何使用os.Signal和context包进行中断和取消操作，以及如何遵循CLI程序的退出状态代码惯例。</p>
<h3>1. 使用log、fmt和errors包进行日志记录和错误处理</h3>
<p>Go标准库中有三个包log、fmt和errors可以帮助你进行日志和错误处理。log包提供了一个简单的接口，可以将格式化的信息写到标准输出或文件中。fmt包则提供了各种格式化字符串和值的函数。errors包提供了创建和操作错误值的函数。</p>
<p>要使用log包，你需要在你的代码中导入它：</p>
<pre><code>import "log"
</code></pre>
<p>然后你可以使用log.Println、log.Printf、log.Fatal和log.Fatalf等函数来输出不同严重程度的信息。比如说：</p>
<pre><code>log.Println("Starting the program...") // 打印带有时间戳的消息
log.Printf("Processing file %s...\n", filename) // 打印一个带时间戳的格式化信息
log.Fatal("Cannot open file: ", err) // 打印一个带有时间戳的错误信息并退出程序
log.Fatalf("Invalid input: %v\n", input) // 打印一个带时间戳的格式化错误信息，并退出程序。
</code></pre>
<p>为了使用fmt包，你需要先在你的代码中导入它：</p>
<pre><code>import "fmt"
</code></pre>
<p>然后你可以使用fmt.Println、fmt.Printf、fmt.Sprintln、fmt.Sprintf等函数以各种方式格式化字符串和值。比如说：</p>
<pre><code>fmt.Println("Hello world!") // 打印一条信息，后面加一个换行符
fmt.Printf("The answer is %d\n", 42) // 打印一条格式化的信息，后面是换行。
s := fmt.Sprintln("Hello world!") // 返回一个带有信息和换行符的字符串。
t := fmt.Sprintf("The answer is %d\n", 42) // 返回一个带有格式化信息和换行的字符串。
</code></pre>
<p>要使用错误包，你同样需要在你的代码中导入它：</p>
<pre><code>import "errors"
</code></pre>
<p>然后你可以使用 errors.New、errors.Unwrap、errors.Is等函数来创建和操作错误值。比如说：</p>
<pre><code>err := errors.New("Something went wrong") // 创建一个带有信息的错误值
cause := errors.Unwrap(err) // 返回错误值的基本原因（如果没有则为nil）。
match := errors.Is(err, io.EOF) // 如果一个错误值与另一个错误值匹配，则返回真（否则返回假）。
</code></pre>
<h3>2. 使用os.Exit和defer语句实现CLI程序的优雅终止</h3>
<p>Go有两个功能可以帮助你优雅地终止CLI程序：os.Exit和defer。os.Exit函数立即退出程序，并给出退出状态代码。defer语句则会在当前函数退出前执行一个函数调用，它常用来执行清理收尾动作，如关闭文件或释放资源。</p>
<p>要使用os.Exit函数，你需要在你的代码中导入os包：</p>
<pre><code>import "os"
</code></pre>
<p>然后你可以使用os.Exit函数，它的整数参数代表退出状态代码。比如说</p>
<pre><code>os.Exit(0) // 以成功的代码退出程序
os.Exit(1) // 以失败代码退出程序
</code></pre>
<p>要使用defer语句，你需要把它写在你想后续执行的函数调用之前。比如说</p>
<pre><code>file, err := os.Open(filename) // 打开一个文件供读取。
if err != nil {
    log.Fatal(err) // 发生错误时退出程序
}
defer file.Close() // 在函数结束时关闭文件。

// 对文件做一些处理...
</code></pre>
<h3>3. 使用os.signal和context包来实现中断和取消操作</h3>
<p>Go有两个包可以帮助你实现中断和取消长期运行的或阻塞的操作，它们是os.signal和context包。os.signal提供了一种从操作系统或其他进程接收信号的方法。context包提供了一种跨越API边界传递取消信号和deadline的方法。</p>
<p>要使用os.signal，你需要先在你的代码中导入它。</p>
<pre><code>import (
  "os"
  "os/signal"
)
</code></pre>
<p>然后你可以使用signal.Notify函数针对感兴趣的信号(如下面的os.Interrupt信号)注册一个接收channel(sig)。比如说：</p>
<pre><code>sig := make(chan os.Signal, 1) // 创建一个带缓冲的信号channel。
signal.Notify(sig, os.Interrupt) // 注册sig以接收中断信号（例如Ctrl-C）。

// 做一些事情...

select {
case &lt;-sig: // 等待来自sig channel的信号
    fmt.Println("被用户中断了")
    os.Exit(1) // 以失败代码退出程序。
default: //如果没有收到信号就执行
    fmt.Println("成功完成")
    os.Exit(0) // 以成功代码退出程序。
}
</code></pre>
<p>要使用上下文包，你需要在你的代码中导入它：</p>
<pre><code>import "context"
</code></pre>
<p>然后你可以使用它的函数，如context.Background、context.WithCancel、context.WithTimeout等来创建和管理Context。Context是一个携带取消信号和deadline的对象，可以跨越API边界。比如说：</p>
<pre><code>ctx := context.Background() // 创建一个空的背景上下文（从不取消）。
ctx, cancel := context.WithCancel(ctx) // 创建一个新的上下文，可以通过调用cancel函数来取消。
defer cancel() // 在函数结束前执行ctx的取消动作

// 将ctx传递给一些接受它作为参数的函数......

select {
case &lt;-ctx.Done(): // 等待来自ctx的取消信号
    fmt.Println("Canceled by parent")
    return ctx.Err() // 从ctx返回一个错误值
default: // 如果没有收到取消信号就执行
    fmt.Println("成功完成")
    return nil // 不返回错误值
}
</code></pre>
<h3>4. CLI程序的退出状态代码惯例</h3>
<p>退出状态代码是一个整数，表示CLI程序是否成功执行完成。CLI程序通过调用os.Exit或从main返回的方式返回退出状态值。其他CLI程序或脚本可以可以检查这些退出状态码，并根据状态码值的不同执行不同的处理操作。</p>
<p>业界有一些关于退出状态代码的约定和扩展，这些约定被许多CLI程序广泛采用。其中一些主要的约定和扩展如下：。</p>
<ul>
<li>退出状态代码为0表示程序执行成功（例如：os.Exit(0) )</li>
<li>非零的退出状态代码表示失败（例如：os.Exit(1) ）。</li>
<li>不同的非零退出状态代码可能表示不同的失败类型或原因（例如：os.Exit(2)表示使用错误，os.Exit(3)表示权限错误等等）。</li>
<li>大于125的退出状态代码可能表示被外部信号终止（例如，os.Exit(130)为被信号中断）。</li>
</ul>
<p>遵循这些约定和扩展可以使你的CLI程序表现的更加一致、可靠并与其他CLI程序兼容。然而，它们不是强制性的，你可以使用任何对你的程序有意义的退出状态代码。例如，一些CLI程序使用高于200的退出状态代码来表示自定义或特定应用的错误（例如，os.Exit(255)表示未知错误）。</p>
<h2>五. 编写文档</h2>
<p>编写优秀CLI程序的另一个重要环节是编写清晰简洁的文档，解释你的程序做什么以及如何使用它。文档可以采取各种形式，如README文件、usage信息、help flag等。在本节中，我们将告诉你如何为你的程序写一个README文件，如何为你的程序写一个有用的usage和help flag等。</p>
<h3>1. 为你的CLI程序写一个清晰简洁的README文件</h3>
<p>README文件是一个文本文件，它提供了关于你的程序的基本信息，如它的名称、描述、用法、安装、依赖性、许可证和联系细节等。它通常是用户或开发者在源代码库或软件包管理器上首次使用你的程序时会看到的内容。</p>
<p>如果你要为Go CLI程序编写一个优秀的README文件，你应该遵循一些最佳实践，比如：</p>
<ul>
<li>使用一个描述性的、醒目的标题，反映你的程序的目的和功能。</li>
<li>提供一个简短的介绍，解释你的程序是做什么的，为什么它是有用的或独特的。</li>
<li>包括一个usage部分，说明如何用不同的标志、参数、子命令和选项来调用你的程序。你可以使用代码块或屏幕截图来说明这些例子。</li>
<li>包括一个安装(install)部分，解释如何在不同的平台上下载和安装你的程序。你可以使用go install、go get、<a href="https://github.com/goreleaser/goreleaser">goreleaser</a>或其他工具来简化这一过程。</li>
<li>指定你的程序的发行许可，并提供一个许可全文的链接。你可以使用<a href="https://spdx.dev/">SPDX标识符</a>来表示许可证类型。</li>
<li>为想要报告问题、请求新功能、贡献代码或提问的用户或开发者提供联系信息。你可以使用github issue、pr、discussion、电子邮件或其他渠道来达到这个目的。</li>
</ul>
<p>以下是一个Go CLI程序的README文件的示例供参考：</p>
<p><img src="https://tonybai.com/wp-content/uploads/the-guide-of-developing-cli-program-in-go-3.png" alt="" /></p>
<h3>2. 为你的CLI程序编写有用的usage和help标志</h3>
<p>usage信息是一段简短的文字，总结了如何使用你的程序及其可用的标志、参数、子命令和选项。它通常在你的程序在没有参数或输入无效的情况下运行时显示。</p>
<p>help标志是一个特殊的标志（通常是-h或&#8211;help），它可以触发显示使用信息和一些关于你的程序的额外信息。</p>
<p>为了给你的Go CLI程序写有用的usage信息和help标志，你应该遵循一些准则，比如说：</p>
<ul>
<li>使用一致而简洁的语法来描述标志、参数、子命令和选项。你可以用方括号“[ ]”表示可选元素，使用角括号“&lt; >”表示必需元素，使用省略号“&#8230;”表示重复元素，使用管道“|”表示备选，使用中划线“-”表示标志(flag)，使用等号“=”表示标志的值等等。</li>
<li>对标志、参数、子命令和选项应使用描述性的名称，以反映其含义和功能。避免使用单字母名称，除非它们非常常见或非常直观（如-v按惯例表示verbose模式）。</li>
<li>为每个标志、参数、子命令和选项提供简短而清晰的描述，解释它们的作用以及它们如何影响你的程序的行为。你可以用圆括号“（ ）”来表达额外的细节或例子。</li>
<li>使用标题或缩进将相关的标志、参数、子命令和选项组合在一起。你也可以用空行或水平线（&#8212;）来分隔usage的不同部分。</li>
<li>在每组中按名称的字母顺序排列标志。在每组中按重要性或逻辑顺序排列参数。在每组中按使用频率排列子命令。</li>
</ul>
<p>git的usage就是一个很好的例子：</p>
<pre><code>$git
usage: git [--version] [--help] [-C &lt;path&gt;] [-c &lt;name&gt;=&lt;value&gt;]
           [--exec-path[=&lt;path&gt;]] [--html-path] [--man-path] [--info-path]
           [-p | --paginate | -P | --no-pager] [--no-replace-objects] [--bare]
           [--git-dir=&lt;path&gt;] [--work-tree=&lt;path&gt;] [--namespace=&lt;name&gt;]
           &lt;command&gt; [&lt;args&gt;]
</code></pre>
<p>结合上面的准则，大家可以细心体会一下。</p>
<h2>六. 测试和发布你的CLI程序</h2>
<p>编写优秀CLI程序的最后一个环节是测试和发布你的程序。测试确保你的程序可以按预期工作，并符合质量标准。发布可以使你的程序可供用户使用和访问。</p>
<p>在本节中，我将说明如何使用testing、testify/assert、mock包对你的代码进行单元测试，如何使用go test、coverage、benchmark工具来运行测试和测量程序性能以及如何使用goreleaser包来构建跨平台的二进制文件。</p>
<h3>1. 使用testing、testify的assert及mock包对你的代码进行单元测试</h3>
<p>单元测试是一种验证单个代码单元（如函数、方法或类型）的正确性和功能的技术。单元测试可以帮助你尽早发现错误，提高代码质量和可维护性，并促进重构和调试。</p>
<p>要为你的Go CLI程序编写单元测试，你应该遵循一些最佳实践：</p>
<ul>
<li>使用内置的测试包来创建测试函数，以Test开头，后面是被测试的函数或方法的名称。例如：func TestSum(t *testing.T) { &#8230; }；</li>
<li>使用&#42;testing.T类型的t参数，使用t.Error、t.Errorf、t.Fatal或t.Fatalf这样的方法报告测试失败。你也可以使用t.Log、t.Logf、t.Skip或t.Skipf这样的方法来提供额外的信息或有条件地跳过测试。</li>
<li>使用<a href="https://tonybai.com/2023/03/15/an-intro-of-go-subtest">Go子测试(sub test)</a>，通过t.Run方法将相关的测试分组。例如：</li>
</ul>
<pre><code>func TestSum(t *testing.T) {
    t.Run("positive numbers", func(t *testing.T) {
        // test sum with positive numbers
    })
    t.Run("negative numbers", func(t *testing.T) {
        // test sum with negative numbers
    })
}
</code></pre>
<ul>
<li>使用表格驱动(table-driven)的测试来运行多个测试用例，比如下面的例子：</li>
</ul>
<pre><code>func TestSum(t *testing.T) {
    tests := []struct{
        name string
        a int
        b int
        want int
    }{
        {"positive numbers", 1, 2, 3},
        {"negative numbers", -1, -2, -3},
        {"zero", 0, 0 ,0},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got := Sum(tt.a , tt.b)
            if got != tt.want {
                t.Errorf("Sum(%d , %d) = %d; want %d", tt.a , tt.b , got , tt.want)
            }
        })
    }
}
</code></pre>
<ul>
<li>使用外部包，如testify/assert或mock来简化你的断言或对外部的依赖性。比如说：</li>
</ul>
<pre><code>import (
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/mock"
)

type Calculator interface {
    Sum(a int , b int) int
}

type MockCalculator struct {
    mock.Mock
}

func (m *MockCalculator) Sum(a int , b int) int {
    args := m.Called(a , b)
    return args.Int(0)
}
</code></pre>
<h3>2. 使用Go的测试、覆盖率、性能基准工具来运行测试和测量性能</h3>
<p>Go提供了一套工具来运行测试和测量你的代码的性能。你可以使用这些工具来确保你的代码按预期工作，检测错误或bug，并优化你的代码以提高速度和效率。</p>
<p>要使用go test、coverage、benchmark工具来运行测试和测量你的Go CLI程序的性能，你应该遵循一些步骤，比如说。</p>
<ul>
<li>将以&#95;test.go结尾的测试文件写在与被测试代码相同的包中。例如：sum_test.go用于测试sum.go。</li>
<li>使用go测试命令来运行一个包中的所有测试或某个特定的测试文件。你也可以使用一些标志，如-v，用于显示verbose的输出，-run用于按名字过滤测试用例，-cover用于显示代码覆盖率，等等。例如：go test -v -cover ./&#8230;</li>
<li>使用go工具cover命令来生成代码覆盖率的HTML报告，并高亮显示代码行。你也可以使用-func这样的标志来显示函数的代码覆盖率，用-html还可以在浏览器中打开覆盖率结果报告等等。例如：go tool cover -html=coverage.out</li>
<li>编写性能基准函数，以Benchmark开头，后面是被测试的函数或方法的名称。使用类型为&#42;testing.B的参数b来控制迭代次数，并使用b.N、b.ReportAllocs等方法控制报告结果的输出。比如说</li>
</ul>
<pre><code>func BenchmarkSum(b *testing.B) {
    for i := 0; i &lt; b.N; i++ {
        Sum(1 , 2)
    }
}
</code></pre>
<ul>
<li>
<p>使用go test -bench命令来运行一个包中的所有性能基准测试或某个特定的基准文件。你也可以使用-benchmem这样的标志来显示内存分配的统计数据，-cpuprofile或-memprofile来生成CPU或内存profile文件等等。例如：go test -bench . -benchmem ./&#8230;</p>
</li>
<li>
<p>使用pprof或benchstat等工具来分析和比较CPU或内存profile文件或基准测试结果。比如说。</p>
</li>
</ul>
<pre><code># Generate CPU profile
go test -cpuprofile cpu.out ./...

# Analyze CPU profile using pprof
go tool pprof cpu.out

# Generate two sets of benchmark results
go test -bench . ./... &gt; old.txt
go test -bench . ./... &gt; new.txt

# Compare benchmark results using benchstat
benchstat old.txt new.txt
</code></pre>
<h3>3. 使用goreleaser包构建跨平台的二进制文件</h3>
<p>构建跨平台二进制文件意味着将你的代码编译成可执行文件，可以在不同的操作系统和架构上运行，如Windows、Linux、Mac OS、ARM等。这可以帮助你向更多的人分发你的程序，使用户更容易安装和运行你的程序而不需要任何依赖或配置。</p>
<p>为了给你的Go CLI程序建立跨平台的二进制文件，你可以使用外部软件包，比如goreleaser等 ，它们可以自动完成程序的构建、打包和发布过程。下面是使用goreleaser包构建程序的一些步骤。</p>
<ul>
<li>使用go get或go install命令安装goreleaser。例如： go install github.com/goreleaser/goreleaser@latest</li>
<li>创建一个配置文件（通常是.goreleaser.yml），指定如何构建和打包你的程序。你可以定制各种选项，如二进制名称、版本、主文件、输出格式、目标平台、压缩、校验和、签名等。例如。</li>
</ul>
<pre><code># .goreleaser.yml
project_name: mycli
builds:
  - main: ./cmd/mycli/main.go
    binary: mycli
    goos:
      - windows
      - darwin
      - linux
    goarch:
      - amd64
      - arm64
archives:
  - format: zip
    name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
    files:
      - LICENSE.txt
      - README.md
checksum:
  name_template: "{{ .ProjectName }}_checksums.txt"
  algorithm: sha256
</code></pre>
<p>运行goreleaser命令，根据配置文件构建和打包你的程序。你也可以使用-snapshot用于测试，-release-notes用于从提交信息中生成发布说明，-rm-dist用于删除之前的构建，等等。例如：goreleaser &#8211;snapshot &#8211;rm-dist。</p>
<p>检查输出文件夹（通常是dist）中生成的二进制文件和其他文件。你也可以使用goreleaser的发布功能将它们上传到源代码库或软件包管理器中。</p>
<h2>七. clig.dev指南要点</h2>
<p>通过上述的系统说明，你现在应该可以设计并使用Go实现出一个CLI程序了。不过本文并非覆盖了clig.dev指南的所有要点，因此，在结束本文之前，我们再来回顾一下clig.dev指南中的要点，大家再体会一下。</p>
<p>前面说过，clig.dev上的cli指南是一个开源指南，可以帮助你写出更好的命令行程序，它采用了传统的UNIX原则，并针对现代的情况进行了更新。</p>
<p>遵循cli准则的一些好处是：</p>
<ul>
<li>你可以创建易于使用、理解和记忆的CLI程序。</li>
<li>你可以设计出能与其他程序进行很好配合的CLI程序，并遵循共同的惯例。</li>
<li>你可以避免让用户和开发者感到沮丧的常见陷阱和错误。</li>
<li>你可以从其他CLI设计者和用户的经验和智慧中学习。</li>
</ul>
<p>下面是该指南的一些要点：</p>
<ul>
<li>理念</li>
</ul>
<p>这一部分解释了好的CLI设计背后的核心原则，如人本设计、可组合性、可发现性、对话性等。例如，以人为本的设计意味着CLI程序对人类来说应该易于使用和理解，而不仅仅是机器。可组合性意味着CLI程序应该通过遵循共同的惯例和标准与其他程序很好地协作。</p>
<ul>
<li>参数和标志</li>
</ul>
<p>这一部分讲述了如何在你的CLI程序中使用位置参数(positional arguments )和标志。它还解释了如何处理默认值、必传参数、布尔标志、多值等。例如，你应该对命令的主要对象或动作使用位置参数，对修改或可选参数使用标志。你还应该使用长短两种形式的标志（如-v或-verbose），并遵循常见的命名模式（如&#8211;help或&#8211;version）。</p>
<ul>
<li>配置</li>
</ul>
<p>这部分介绍了如何使用配置文件和环境变量来为你的CLI程序存储持久的设置。它还解释了如何处理配置选项的优先级、验证、文档等。例如，你应该使用配置文件来处理用户很少改变的设置，或者是针对某个项目或环境的设置。对于特定于环境或会话的设置（如凭证或路径），你也应该使用环境变量。</p>
<ul>
<li>输出</li>
</ul>
<p>这部分介绍了如何格式化和展示你的CLI程序的输出。它还解释了如何处理输出verbose级别、进度指示器、颜色、表格等。例如，你应该使用标准输出（stdout）进行正常的输出，这样输出的信息可以通过管道输送到其他程序或文件。你还应该使用标准错误（stderr）来处理不属于正常输出流的错误或警告。</p>
<ul>
<li>错误</li>
</ul>
<p>这部分介绍了如何在你的CLI程序中优雅地处理错误。它还解释了如何使用退出状态码、错误信息、堆栈跟踪等。例如，你应该使用表明错误类型的退出代码（如0代表成功，1代表一般错误）。你还应该使用简洁明了的错误信息，解释出错的原因以及如何解决。</p>
<ul>
<li>子命令</li>
</ul>
<p>这部分介绍了当CLI程序有多种操作或操作模式时，如何在CLI程序中使用子命令。它还解释了如何分层构建子命令，组织帮助文本，以及处理常见的子命令（如help或version）。例如，当你的程序有不同的功能，需要不同的参数或标志时（如git clone或git commit），你应该使用子命令。你还应该提供一个默认的子命令，或者在没有给出子命令时提供一个可用的子命令列表。</p>
<p>业界有许多精心设计的CLI工具的例子，它们都遵循cli准则，大家可以通过使用来深刻体会一下这些准则。下面是一些这样的CLI工具的例子：</p>
<ul>
<li>
<p>httpie：一个命令行HTTP客户端，具有直观的UI，支持JSON，语法高亮，类似wget的下载，插件等功能。例如，Httpie使用清晰简洁的语法进行HTTP请求，支持多种输出格式和颜色，优雅地处理错误并提供有用的文档。</p>
</li>
<li>
<p>git：一个分布式的版本控制系统，让你管理你的源代码并与其他开发者合作。例如，Git使用子命令进行不同的操作（如git clone或git commit），遵循通用的标志（如-v或-verbose），提供有用的反馈和建议（如git status或git help），并支持配置文件和环境变量。</p>
</li>
<li>
<p>npm：一个JavaScript的包管理器，让你为你的项目安装和管理依赖性。例如，NPM使用一个简单的命令结构（npm <command> [args]），提供一个简洁的初始帮助信息，有更详细的选项（npm help npm），支持标签完成和合理的默认值，并允许你通过配置文件（.npmrc）自定义设置。</p>
</li>
</ul>
<h2>八. 小结</h2>
<p>在这篇文章中，我们系统说明了如何编写出遵循命令行接口指南的Go CLI程序。</p>
<p>你学习了如何设置Go环境、设计命令行接口、处理错误和信号、编写文档、使用各种工具和软件包测试和发布程序。你还看到了一些代码和配置文件的例子。通过遵循这些准则和最佳实践，你可以创建一个用户友好、健壮和可靠的CLI程序。</p>
<p>最后我们回顾了clig.dev的指南要点，希望你能更深刻理解这些要点的含义。</p>
<p>我希望你喜欢这篇文章并认为它很有用。如果你有任何问题或反馈，请随时联系我。编码愉快！</p>
<blockquote>
<p>注：本文系与New Bing Chat联合完成，旨在验证如何基于AIGC能力构思和编写长篇文章。文章内容的正确性经过笔者全面审校，可放心阅读。</p>
</blockquote>
<hr />
<p><a href="https://wx.zsxq.com/dweb2/index/group/51284458844544">“Gopher部落”知识星球</a>旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！</p>
<p><img src="http://image.tonybai.com/img/tonybai/gopher-tribe-zsxq-small-card.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/go-programming-from-beginner-to-master-qr.png" alt="img{512x368}" /></p>
<p><img src="http://image.tonybai.com/img/tonybai/go-first-course-banner.png" alt="img{512x368}" /><br />
<img src="http://image.tonybai.com/img/tonybai/imooc-go-column-pgo-with-qr.jpg" alt="img{512x368}" /></p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<ul>
<li>微博(暂不可用)：https://weibo.com/bigwhite20xx</li>
<li>微博2：https://weibo.com/u/6484441286</li>
<li>博客：tonybai.com</li>
<li>github: https://github.com/bigwhite</li>
</ul>
<p><img src="http://image.tonybai.com/img/tonybai/iamtonybai-wechat-qr.png" alt="" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2023, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2023/03/25/the-guide-of-developing-cli-program-in-go/feed/</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
		<item>
		<title>Go 1.15中值得关注的几个变化</title>
		<link>https://tonybai.com/2020/10/11/some-changes-in-go-1-15/</link>
		<comments>https://tonybai.com/2020/10/11/some-changes-in-go-1-15/#comments</comments>
		<pubDate>Sun, 11 Oct 2020 03:10:06 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[arm64]]></category>
		<category><![CDATA[cache]]></category>
		<category><![CDATA[Darwin]]></category>
		<category><![CDATA[GNU]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[go1.11]]></category>
		<category><![CDATA[go1.13]]></category>
		<category><![CDATA[go1.14]]></category>
		<category><![CDATA[go1.15]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[gomodule]]></category>
		<category><![CDATA[GOPATH]]></category>
		<category><![CDATA[GOPROXY]]></category>
		<category><![CDATA[GOROOT]]></category>
		<category><![CDATA[Interface]]></category>
		<category><![CDATA[json]]></category>
		<category><![CDATA[linker]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Module]]></category>
		<category><![CDATA[objdump]]></category>
		<category><![CDATA[Package]]></category>
		<category><![CDATA[panic]]></category>
		<category><![CDATA[Reflect]]></category>
		<category><![CDATA[RISC-V]]></category>
		<category><![CDATA[tzdata]]></category>
		<category><![CDATA[unsafe]]></category>
		<category><![CDATA[Windows]]></category>
		<category><![CDATA[zoneinfo]]></category>
		<category><![CDATA[优化]]></category>
		<category><![CDATA[内存分配]]></category>
		<category><![CDATA[包]]></category>
		<category><![CDATA[标准库]]></category>
		<category><![CDATA[汇编]]></category>
		<category><![CDATA[编译器]]></category>
		<category><![CDATA[链接器]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=2955</guid>
		<description><![CDATA[Go 1.15版本在8月12日就正式发布了，给我的感觉就是发布的挺痛快^_^。这种感觉来自与之前版本发布时间的对比：Go 1.13版本发布于当年的9月4日，更早的Go 1.11版本发布于当年的8月25日。 不过这个时间恰与我家二宝出生和老婆月子时期有重叠，每天照顾孩子团团转的我实在抽不出时间研究Go 1.15的变化:(。如今，我逐渐从照顾二宝的工作中脱离出来^_^，于是“Go x.xx版本值得关注的几个变化”系列将继续下去。关注Go语言的演变对掌握和精通Go语言大有裨益，凡是致力于成为一名高级Gopher的读者都应该密切关注Go的演进。 截至写稿时，Go 1.15最新版是Go 1.15.2。Go 1.15一如既往的遵循Go1兼容性承诺。语言规范方面没有任何变化。可以说这是一个“面子”上变化较小的一个版本，但“里子”的变化还是不少的，在本文中我就和各位读者一起就重要变化逐一了解一下。 一. 平台移植性 Go 1.15版本不再对darwin/386和darwin/arm两个32位平台提供支持了。Go 1.15及以后版本仅对darwin/amd64和darwin/arm64版本提供支持。并且不再对macOS 10.12版本之前的版本提供支持。 Go 1.14版本中，Go编译器在被传入-race和-msan的情况下，默认会执行-d=checkptr，即对unsafe.Pointer的使用进行合法性检查。-d=checkptr主要检查两项内容： 当将unsafe.Pointer转型为&#42;T时，T的内存对齐系数不能高于原地址的； 做完指针算术后，转换后的unsafe.Pointer仍应指向原先Go堆对象 但在Go 1.14中，这个检查并不适用于Windows操作系统。Go 1.15中增加了对windows系统的支持。 对于RISC-V架构，Go社区展现出十分积极的姿态，早在Go 1.11版本，Go就为RISC-V cpu架构预留了GOARCH值：riscv和riscv64。Go 1.14版本则为64bit RISC-V提供了在linux上的实验性支持(GOOS=linux, GOARCH=riscv64)。在Go 1.15版本中，Go在GOOS=linux, GOARCH=riscv64的环境下的稳定性和性能得到持续提升，并且已经可以支持goroutine异步抢占式调度了。 二. 工具链 1. GOPROXY新增以管道符为分隔符的代理列表值 在Go 1.13版本中，GOPROXY支持设置为多个proxy的列表，多个proxy之间采用逗号分隔。Go工具链会按顺序尝试列表中的proxy以获取依赖包数据，但是当有proxy server服务不可达或者是返回的http状态码不是404也不是410时，go会终止数据获取。但是当列表中的proxy server返回其他错误时，Go命令不会向GOPROXY列表中的下一个值所代表的的proxy server发起请求，这种行为模式没能让所有gopher满意，很多Gopher认为Go工具链应该向后面的proxy server请求，直到所有proxy server都返回失败。Go 1.15版本满足了Go社区的需求，新增以管道符“&#124;”为分隔符的代理列表值。如果GOPROXY配置的proxy server列表值以管道符分隔，则无论某个proxy server返回什么错误码，Go命令都会向列表中的下一个proxy server发起新的尝试请求。 注：Go 1.15版本中GOPROXY环境变量的默认值依旧为https://proxy.golang.org,direct。 2. module cache的存储路径可设置 Go module机制自打在Go 1.11版本中以试验特性的方式引入时就将module的本地缓存默认放在了\$GOPATH/pkg/mod下（如果没有显式设置GOPATH，那么默认值将是~/go；如果GOPATH下面配置了多个路径，那么选择第一个路径），一直到Go [...]]]></description>
			<content:encoded><![CDATA[<p><img src="https://tonybai.com/wp-content/uploads/go-1.15-1.png" alt="img{512x368}" /></p>
<p><a href="https://tip.golang.org/doc/go1.15">Go 1.15版本</a>在8月12日就正式发布了，给我的感觉就是发布的挺痛快^_^。这种感觉来自与之前版本发布时间的对比：<a href="https://tonybai.com/2019/10/27/some-changes-in-go-1-13/">Go 1.13版本</a>发布于当年的9月4日，更早的<a href="https://tonybai.com/2018/11/19/some-changes-in-go-1-11/">Go 1.11版本</a>发布于当年的8月25日。</p>
<p>不过这个时间恰与我家<a href="https://tonybai.com/2020/07/29/my-second-daughter-was-born/">二宝出生</a>和老婆月子时期有重叠，每天照顾孩子团团转的我实在抽不出时间研究Go 1.15的变化:(。如今，我逐渐从照顾二宝的工作中脱离出来^_^，于是“Go x.xx版本值得关注的几个变化”系列将继续下去。关注Go语言的演变对掌握和精通Go语言大有裨益，凡是致力于成为一名高级Gopher的读者都应该密切关注Go的演进。<br />
截至写稿时，Go 1.15最新版是Go 1.15.2。Go 1.15一如既往的遵循<a href="https://tip.golang.org/doc/go1compat.html">Go1兼容性承诺</a>。<a href="https://tip.golang.org/ref/spec">语言规范</a>方面没有任何变化。可以说这是一个“面子”上变化较小的一个版本，但“里子”的变化还是不少的，在本文中我就和各位读者一起就重要变化逐一了解一下。</p>
<h3>一. 平台移植性</h3>
<p>Go 1.15版本不再对darwin/386和darwin/arm两个32位平台提供支持了。Go 1.15及以后版本仅对darwin/amd64和darwin/arm64版本提供支持。并且不再对macOS 10.12版本之前的版本提供支持。</p>
<p><a href="https://tonybai.com/2020/03/08/some-changes-in-go-1-14/">Go 1.14版本</a>中，Go编译器在被传入-race和-msan的情况下，默认会执行<strong>-d=checkptr</strong>，即对unsafe.Pointer的使用进行<a href="https://github.com/golang/go/issues/34964">合法性检查</a>。<strong>-d=checkptr</strong>主要检查两项内容：</p>
<ul>
<li>
<p>当将unsafe.Pointer转型为&#42;T时，T的内存对齐系数不能高于原地址的；</p>
</li>
<li>
<p>做完指针算术后，转换后的unsafe.Pointer仍应指向原先Go堆对象</p>
</li>
</ul>
<p>但在Go 1.14中，这个检查并不适用于Windows操作系统。Go 1.15中增加了对windows系统的支持。</p>
<p>对于<a href="https://riscv.org">RISC-V</a>架构，Go社区展现出十分积极的姿态，早在<a href="https://tonybai.com/2018/11/19/some-changes-in-go-1-11/">Go 1.11版本</a>，Go就为RISC-V cpu架构预留了GOARCH值：riscv和riscv64。<a href="https://tonybai.com/2020/03/08/some-changes-in-go-1-14/">Go 1.14版本</a>则为64bit RISC-V提供了在linux上的实验性支持(GOOS=linux, GOARCH=riscv64)。在Go 1.15版本中，Go在GOOS=linux, GOARCH=riscv64的环境下的稳定性和性能得到持续提升，并且已经可以支持goroutine异步抢占式调度了。</p>
<h3>二. 工具链</h3>
<h4>1. GOPROXY新增以管道符为分隔符的代理列表值</h4>
<p>在<a href="https://tonybai.com/2019/10/27/some-changes-in-go-1-13/">Go 1.13版本</a>中，<a href="https://tonybai.com/2018/11/26/hello-go-module-proxy/">GOPROXY</a>支持设置为多个proxy的列表，多个proxy之间采用逗号分隔。Go工具链会按顺序尝试列表中的proxy以获取依赖包数据，但是当有proxy server服务不可达或者是返回的http状态码不是404也不是410时，go会终止数据获取。但是当列表中的proxy server返回其他错误时，Go命令不会向GOPROXY列表中的下一个值所代表的的proxy server发起请求，这种行为模式没能让所有gopher满意，<strong>很多Gopher认为Go工具链应该向后面的proxy server请求，直到所有proxy server都返回失败</strong>。Go 1.15版本满足了Go社区的需求，新增以管道符“|”为分隔符的代理列表值。如果GOPROXY配置的proxy server列表值以管道符分隔，则无论某个proxy server返回什么错误码，Go命令都会向列表中的下一个proxy server发起新的尝试请求。</p>
<blockquote>
<p>注：Go 1.15版本中GOPROXY环境变量的默认值依旧为<strong>https://proxy.golang.org,direct</strong>。</p>
</blockquote>
<h4>2. module cache的存储路径可设置</h4>
<p><a href="https://tonybai.com/2019/06/03/the-practice-of-upgrading-major-version-under-go-module/">Go module机制</a>自打在<a href="https://tonybai.com/2018/11/19/some-changes-in-go-1-11/">Go 1.11版本</a>中以试验特性的方式引入时就将module的本地缓存默认放在了<strong>\$GOPATH/pkg/mod</strong>下（如果没有显式设置GOPATH，那么默认值将是<strong>~/go</strong>；如果GOPATH下面配置了多个路径，那么选择第一个路径），一直到Go 1.14版本，这个位置都是无法配置的。</p>
<p>Go module的引入为去除GOPATH提供了前提，于是module cache的位置也要尽量与GOPATH“脱钩”：Go 1.15提供了GOMODCACHE环境变量用于自定义module cache的存放位置。如果没有显式设置GOMODCACHE，那么module cache的默认存储路径依然是<strong>\$GOPATH/pkg/mod</strong>。</p>
<h3>三. 运行时、编译器和链接器</h3>
<h4>1. panic展现形式变化</h4>
<p>在Go 1.15之前，如果传给panic的值是bool, complex64, complex128, float32, float64, int, int8, int16, int32, int64, string, uint, uint8, uint16, uint32, uint64, uintptr等原生类型的值，那么panic在触发时会输出具体的值，比如：</p>
<pre><code>// go1.15-examples/runtime/panic.go

package main

func foo() {
    var i uint32 = 17
    panic(i)
}

func main() {
    foo()
}
</code></pre>
<p>使用Go 1.14运行上述代码，得到如下结果：</p>
<pre><code>$go run panic.go
panic: 17

goroutine 1 [running]:
main.foo(...)
    /Users/tonybai/go/src/github.com/bigwhite/experiments/go1.15-examples/runtime/panic.go:5
main.main()
    /Users/tonybai/go/src/github.com/bigwhite/experiments/go1.15-examples/runtime/panic.go:9 +0x39
exit status 2
</code></pre>
<p>Go 1.15版本亦是如此。但是对于派生于上述原生类型的自定义类型而言，Go 1.14只是输出变量地址：</p>
<pre><code>// go1.15-examples/runtime/panic.go

package main

type myint uint32

func bar() {
    var i myint = 27
    panic(i)
}

func main() {
    bar()
}
</code></pre>
<p>使用Go 1.14运行上述代码：</p>
<pre><code>$go run panic.go
panic: (main.myint) (0x105fca0,0xc00008e000)

goroutine 1 [running]:
main.bar(...)
    /Users/tonybai/go/src/github.com/bigwhite/experiments/go1.15-examples/runtime/panic.go:12
main.main()
    /Users/tonybai/go/src/github.com/bigwhite/experiments/go1.15-examples/runtime/panic.go:17 +0x39
exit status 2

</code></pre>
<p>Go 1.15针对此情况作了展示优化，即便是派生于这些原生类型的自定义类型变量，panic也可以输出其值。使用Go 1.15运行上述代码的结果如下：</p>
<pre><code>$go run panic.go
panic: main.myint(27)

goroutine 1 [running]:
main.bar(...)
    /Users/tonybai/go/src/github.com/bigwhite/experiments/go1.15-examples/runtime/panic.go:12
main.main()
    /Users/tonybai/go/src/github.com/bigwhite/experiments/go1.15-examples/runtime/panic.go:17 +0x39
exit status 2

</code></pre>
<h4>2. 将小整数([0,255])转换为interface类型值时将不会额外分配内存</h4>
<p>Go 1.15在runtime/iface.go中做了一些优化改动：增加一个名为staticuint64s的数组，预先为[0,255]这256个数分配了内存。然后在convT16、convT32等运行时转换函数中判断要转换的整型值是否小于256(len(staticuint64s))，如果小于，则返回staticuint64s数组中对应的值的地址；否则调用mallocgc分配新内存。</p>
<pre><code>$GOROOT/src/runtime/iface.go

// staticuint64s is used to avoid allocating in convTx for small integer values.
var staticuint64s = [...]uint64{
        0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
        0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
        0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
        0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
        0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27,
        0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,
        0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37,

        ... ...

        0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7,
        0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff,

}

func convT16(val uint16) (x unsafe.Pointer) {
        if val &lt; uint16(len(staticuint64s)) {
                x = unsafe.Pointer(&amp;staticuint64s[val])
                if sys.BigEndian {
                        x = add(x, 6)
                }
        } else {
                x = mallocgc(2, uint16Type, false)
                *(*uint16)(x) = val
        }
        return
}

func convT32(val uint32) (x unsafe.Pointer) {
        if val &lt; uint32(len(staticuint64s)) {
                x = unsafe.Pointer(&amp;staticuint64s[val])
                if sys.BigEndian {
                        x = add(x, 4)
                }
        } else {
                x = mallocgc(4, uint32Type, false)
                *(*uint32)(x) = val
        }
        return
}

</code></pre>
<p>我们可以用下面例子来验证一下：</p>
<pre><code>// go1.15-examples/runtime/tinyint2interface.go

package main

import (
    "math/rand"
)

func convertSmallInteger() interface{} {
    i := rand.Intn(256)
    var j interface{} = i
    return j
}

func main() {
    for i := 0; i &lt; 100000000; i++ {
        convertSmallInteger()
    }
}

</code></pre>
<p>我们分别用go 1.14和go 1.15.2编译这个源文件（注意关闭内联等优化，否则很可能看不出效果）：</p>
<pre><code>// go 1.14

go build  -gcflags="-N -l" -o tinyint2interface-go14 tinyint2interface.go

// go 1.15.2

go build  -gcflags="-N -l" -o tinyint2interface-go15 tinyint2interface.go

</code></pre>
<p>我们使用下面命令输出程序执行时每次GC的信息：</p>
<pre><code>$env GODEBUG=gctrace=1 ./tinyint2interface-go14
gc 1 @0.025s 0%: 0.009+0.18+0.021 ms clock, 0.079+0.079/0/0.20+0.17 ms cpu, 4-&gt;4-&gt;0 MB, 5 MB goal, 8 P
gc 2 @0.047s 0%: 0.003+0.14+0.013 ms clock, 0.031+0.099/0.064/0.037+0.10 ms cpu, 4-&gt;4-&gt;0 MB, 5 MB goal, 8 P
gc 3 @0.064s 0%: 0.008+0.20+0.016 ms clock, 0.071+0.071/0.018/0.081+0.13 ms cpu, 4-&gt;4-&gt;0 MB, 5 MB goal, 8 P
gc 4 @0.081s 0%: 0.005+0.14+0.013 ms clock, 0.047+0.059/0.023/0.032+0.10 ms cpu, 4-&gt;4-&gt;0 MB, 5 MB goal, 8 P
gc 5 @0.098s 0%: 0.005+0.10+0.017 ms clock, 0.042+0.073/0.027/0.080+0.13 ms cpu, 4-&gt;4-&gt;0 MB, 5 MB goal, 8 P

... ...

gc 192 @3.264s 0%: 0.003+0.11+0.013 ms clock, 0.024+0.060/0.005/0.035+0.11 ms cpu, 4-&gt;4-&gt;0 MB, 5 MB goal, 8 P
gc 193 @3.281s 0%: 0.005+0.13+0.032 ms clock, 0.042+0.075/0.041/0.050+0.25 ms cpu, 4-&gt;4-&gt;0 MB, 5 MB goal, 8 P
gc 194 @3.298s 0%: 0.004+0.12+0.013 ms clock, 0.033+0.072/0.030/0.033+0.10 ms cpu, 4-&gt;4-&gt;0 MB, 5 MB goal, 8 P
gc 195 @3.315s 0%: 0.003+0.17+0.023 ms clock, 0.029+0.062/0.055/0.024+0.18 ms cpu, 4-&gt;4-&gt;0 MB, 5 MB goal, 8 P

$env GODEBUG=gctrace=1 ./tinyint2interface-go15
</code></pre>
<p>我们看到和go 1.14编译的程序不断分配内存，不断导致GC相比，go1.15.2没有输出GC信息，间接证实了小整数转interface变量值时不会触发内存分配。</p>
<h4>3. 加入更现代化的链接器(linker)</h4>
<p>一个新版的<a href="https://golang.org/s/better-linker">现代化linker</a>正在逐渐加入到Go中，Go 1.15是新版linker的起点。后续若干版本，linker优化会逐步加入进来。在Go 1.15中，对于大型项目，新链接器的性能要提高20%，内存占用减少30%。</p>
<h4>4. objdump支持输出GNU汇编语法</h4>
<p>go 1.15为objdump工具增加了-gnu选项，<strong>以在Go汇编的后面，辅助输出GNU汇编，便于对照</strong>：</p>
<pre><code>// go 1.14：

$go tool objdump -S tinyint2interface-go15|more
TEXT go.buildid(SB)

  0x1001000             ff20                    JMP 0(AX)
  0x1001002             476f                    OUTSD DS:0(SI), DX
  0x1001004             206275                  ANDB AH, 0x75(DX)
  0x1001007             696c642049443a20        IMULL $0x203a4449, 0x20(SP), BP
... ...

//go 1.15.2：

$go tool objdump  -S -gnu tinyint2interface-go15|more
TEXT go.buildid(SB)

  0x1001000             ff20                    JMP 0(AX)                            // jmpq *(%rax)           

  0x1001002             476f                    OUTSD DS:0(SI), DX                   // rex.RXB outsl %ds:(%rsi),(%dx)
  0x1001004             206275                  ANDB AH, 0x75(DX)                    // and %ah,0x75(%rdx)     

  0x1001007             696c642049443a20        IMULL $0x203a4449, 0x20(SP), BP      // imul $0x203a4449,0x20(%rsp,%riz,2),%ebp

... ...

</code></pre>
<h3>四. 标准库</h3>
<p>和以往发布的版本一样，标准库有大量小改动，这里挑出几个笔者感兴趣的和大家一起看一下。</p>
<h4>1. 增加tzdata包</h4>
<p>Go time包中很多方法依赖时区数据，但不是所有平台上都自带时区数据。Go time包会以下面顺序搜寻时区数据：</p>
<pre><code>- ZONEINFO环境变量指示的路径中

- 在类Unix系统中一些常见的存放时区数据的路径（zoneinfo_unix.go中的zoneSources数组变量中存放这些常见路径）：

    "/usr/share/zoneinfo/",
    "/usr/share/lib/zoneinfo/",
    "/usr/lib/locale/TZ/"

- 如果平台没有，则尝试使用$GOROOT/lib/time/zoneinfo.zip这个随着go发布包一起发布的时区数据。但在应用部署的环境中，很大可能不会进行go安装。

</code></pre>
<p>如果go应用找不到时区数据，那么go应用运行将会受到影响，就如下面这个例子：</p>
<pre><code>// go1.15-examples/stdlib/tzdata.go

package main

import (
    "fmt"
    "time"
)

func main() {
    loc, err := time.LoadLocation("America/New_York")
    if err != nil {
        fmt.Println("LoadLocation error:", err)
        return
    }
    fmt.Println("LoadLocation is:", loc)
}

</code></pre>
<p>我们移除系统的时区数据(比如将/usr/share/zoneinfo改名)和Go安装包自带的zoneinfo.zip(改个名)后，在Go 1.15.2下运行该示例：</p>
<pre><code>$ go run tzdata.go
LoadLocation error: unknown time zone America/New_York

</code></pre>
<p>为此，Go 1.15提供了一个将时区数据嵌入到Go应用二进制文件中的方法：<strong>导入time/tzdata包</strong>：</p>
<pre><code>// go1.15-examples/stdlib/tzdata.go

package main

import (
    "fmt"
    "time"
    _ "time/tzdata"
)

func main() {
    loc, err := time.LoadLocation("America/New_York")
    if err != nil {
        fmt.Println("LoadLocation error:", err)
        return
    }
    fmt.Println("LoadLocation is:", loc)
}

</code></pre>
<p>我们再用go 1.15.2运行一下上述导入tzdata包的例子：</p>
<pre><code>$go run testtimezone.go
LoadLocation is: America/New_York

</code></pre>
<p>不过由于附带tzdata数据，应用二进制文件的size会增大大约800k，下面是在ubuntu下的实测值：</p>
<pre><code>-rwxr-xr-x 1 root root 2.0M Oct 11 02:42 tzdata-withouttzdata*
-rwxr-xr-x 1 root root 2.8M Oct 11 02:42 tzdata-withtzdata*

</code></pre>
<h4>2. 增加json解码限制</h4>
<p>json包是日常使用最多的go标准库包之一，在Go 1.15中，go按照json规范的要求，为json的解码增加了一层限制：</p>
<pre><code>// json规范要求

//https://tools.ietf.org/html/rfc7159#section-9

A JSON parser transforms a JSON text into another representation.  A
   JSON parser MUST accept all texts that conform to the JSON grammar.
   A JSON parser MAY accept non-JSON forms or extensions.

   An implementation may set limits on the size of texts that it
   accepts.  An implementation may set limits on the maximum depth of
   nesting.  An implementation may set limits on the range and precision
   of numbers.  An implementation may set limits on the length and
   character contents of strings.

</code></pre>
<p>这个限制就是增加了一个对json文本最大缩进深度值：</p>
<pre><code>// $GOROOT/src/encoding/json/scanner.go

// This limits the max nesting depth to prevent stack overflow.
// This is permitted by https://tools.ietf.org/html/rfc7159#section-9
const maxNestingDepth = 10000
</code></pre>
<p>如果一旦传入的json文本数据缩进深度超过maxNestingDepth，那json包就会panic。当然，绝大多数情况下，我们是碰不到缩进10000层的超大json文本的。因此，该limit对于99.9999%的gopher都没啥影响。</p>
<h4>3. reflect包</h4>
<p>Go 1.15版本之前reflect包<a href="https://github.com/golang/go/issues/38521">存在一处行为不一致的问题</a>，我们看下面例子(例子来源于https://play.golang.org/p/Jnga2_6Rmdf)：</p>
<pre><code>// go1.15-examples/stdlib/reflect.go

package main

import "reflect"

type u struct{}

func (u) M() { println("M") }

type t struct {
    u
    u2 u
}

func call(v reflect.Value) {
    defer func() {
        if err := recover(); err != nil {
            println(err.(string))
        }
    }()
    v.Method(0).Call(nil)
}

func main() {
    v := reflect.ValueOf(t{}) // v := t{}
    call(v)                   // v.M()
    call(v.Field(0))          // v.u.M()
    call(v.Field(1))          // v.u2.M()
}

</code></pre>
<p>我们使用Go 1.14版本运行该示例：</p>
<pre><code>$go run reflect.go
M
M
reflect: reflect.flag.mustBeExported using value obtained using unexported field

</code></pre>
<p>我们看到同为类型t中的非导出字段(field)的u和u2(u是以嵌入类型方式称为类型t的字段的)，通过reflect包可以调用字段u的导出方法(如输出中的第二行的M)，却无法调用非导出字段u2的导出方法（如输出中的第三行的panic信息）。</p>
<p>这种不一致在Go 1.15版本中被修复，我们使用Go 1.15.2运行上述示例：</p>
<pre><code>$go run reflect.go
M
reflect: reflect.Value.Call using value obtained using unexported field
reflect: reflect.Value.Call using value obtained using unexported field

</code></pre>
<p>我们看到reflect无法调用非导出字段u和u2的导出方法了。但是reflect依然可以通过提升到类型t的方法来间接使用u的导出方法，正如运行结果中的第一行输出。<br />
<strong>这一改动可能会影响到遗留代码中使用reflect调用以类型嵌入形式存在的非导出字段方法的代码</strong>，如果你的代码中存在这样的问题，可以直接通过提升(promote)到包裹类型(如例子中的t)中的方法（如例子中的call(v)）来替代之前的方式。</p>
<h3>五. 小结</h3>
<p>由于Go 1.15删除了一些GC元数据和一些无用的类型元数据，Go 1.15编译出的二进制文件size会减少5%左右。我用一个中等规模的go项目实测了一下：</p>
<pre><code>-rwxr-xr-x   1 tonybai  staff    23M 10 10 16:54 yunxind*
-rwxr-xr-x   1 tonybai  staff    24M  9 30 11:20 yunxind-go14*

</code></pre>
<p>二进制文件size的确有变小，大约4%-5%。</p>
<p><strong>如果你还没有升级到Go 1.15，那么现在正是时候</strong>。</p>
<p>本文中涉及的代码可以在<a href="https://github.com/bigwhite/experiments/tree/master/go1.15-examples">这里</a>下载。https://github.com/bigwhite/experiments/tree/master/go1.15-examples</p>
<hr />
<p>我的Go技术专栏：“<a href="https://www.imooc.com/read/87">改善Go语⾔编程质量的50个有效实践</a>”上线了，欢迎大家订阅学习！</p>
<p>我的网课“<a href="https://coding.imooc.com/class/284.html">Kubernetes实战：高可用集群搭建、配置、运维与应用</a>”在慕课网上线了，感谢小伙伴们学习支持！</p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/<br />
smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。</p>
<p>2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<p>微博：https://weibo.com/bigwhite20xx<br />
微信公众号：iamtonybai<br />
博客：tonybai.com<br />
github: https://github.com/bigwhite</p>
<p>微信赞赏：<br />
<img src="https://tonybai.com/wp-content/uploads/wechat-zanshang-code-512x512.jpg" alt="img{512x368}" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2020, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2020/10/11/some-changes-in-go-1-15/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Recommended C Style and Coding Standards中文版全文</title>
		<link>https://tonybai.com/2013/11/26/the-full-text-of-recommended-c-style-and-coding-standards/</link>
		<comments>https://tonybai.com/2013/11/26/the-full-text-of-recommended-c-style-and-coding-standards/#comments</comments>
		<pubDate>Tue, 26 Nov 2013 12:46:33 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[ANSI-C]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[Compiler]]></category>
		<category><![CDATA[C标准]]></category>
		<category><![CDATA[Debug]]></category>
		<category><![CDATA[GCC]]></category>
		<category><![CDATA[GNU]]></category>
		<category><![CDATA[Google]]></category>
		<category><![CDATA[Lint]]></category>
		<category><![CDATA[Macro]]></category>
		<category><![CDATA[Make]]></category>
		<category><![CDATA[Opensource]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[内存对齐]]></category>
		<category><![CDATA[博客]]></category>
		<category><![CDATA[原型]]></category>
		<category><![CDATA[可移植性]]></category>
		<category><![CDATA[宏]]></category>
		<category><![CDATA[开源]]></category>
		<category><![CDATA[程序员]]></category>
		<category><![CDATA[编译器]]></category>
		<category><![CDATA[翻译]]></category>
		<category><![CDATA[调试]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=1454</guid>
		<description><![CDATA[今天无意中打开了托管在Google Code上的&#8220;Recommended C Style and Coding Standards&#8221;翻译项目，忽感觉通过目录链接的方式查看译文缺少整体感，于是花了点时间将译文全文以single page的形式贴在博客里面，方便大家查看，也算是对该翻译内容的一个备份吧。 C语言编码风格和标准 0. 摘要 本文翻译自《Recommended C Style and Coding Standards》。 作者信息： L.W. Cannon (Bell Labs) R.A. Elliott (Bell Labs) L.W. Kirchhoff (Bell Labs) J.H. Miller (Bell Labs) J.M. Milner (Bell Labs) R.W. Mitze (Bell Labs) E.P. Schan (Bell Labs) N.O. Whittington (Bell Labs) Henry Spencer (Zoology Computer Systems, [...]]]></description>
			<content:encoded><![CDATA[<p><big><small>今天无意中打开了托管在Google Code上的&ldquo;<a href="http://code.google.com/p/recommended-c-style-and-coding-standards-cn/">Recommended C Style and Coding Standards</a>&rdquo;翻译项目，忽感觉通过目录链接的方式查看译文缺少整体感，于是花了点时间将译文全文以single page的形式贴在博客里面，方便大家查看，也算是对该翻译内容的一个备份吧。</small></big></p>
<p><big><small><b><big>C语言编码风格和标准</big></b> </small></big></p>
<p><big><b>0. 摘要</b></big></p>
<p>本文翻译自《Recommended C Style and Coding Standards》。</p>
<p>作者信息：</p>
<p><font face="Courier New">L.W. Cannon (Bell Labs)<br />
	R.A. Elliott (Bell Labs)<br />
	L.W. Kirchhoff (Bell Labs)<br />
	J.H. Miller (Bell Labs)<br />
	J.M. Milner (Bell Labs)<br />
	R.W. Mitze (Bell Labs)<br />
	E.P. Schan (Bell Labs)<br />
	N.O. Whittington (Bell Labs)<br />
	Henry Spencer (Zoology Computer Systems, University of Toronto)<br />
	David Keppel (EECS, UC Berkeley, CS&amp;E, University of Washington)<br />
	Mark Brader (SoftQuad? Incorporated, Toronto)</font></p>
<p>本文是《Indian Hill C Style and Coding Standards》的更新版本，上面提到的最后三位作者对其进行了修改。本文主要介绍了一种C程序的推荐编码标准，内容着重于讲述编码风格，而不是功能 组织(Functional Organization)。</p>
<p><big><b>1. 简介</b></big></p>
<p>本文档修改于AT&amp;T Indian Hill实验室内部成立的一个委员会的一份文档，旨在于建立一套通用的编码标准并推荐给Indian Hill社区。</p>
<p>本文主要讲述编码风格。良好的风格能够鼓励大家形成一致的代码布局，提高代码可移植性并且减少错误数量。</p>
<p>本文不关注功能组织，或是一些诸如如何使用goto的一般话题。我们尝试将之前的有关C代码风格的文档整合到一套统一的标准中，这套标准将适合于 任何使用C语言的工程，当然还是会有部分内容是针对一些特定系统的。另外不可避免地是这些标准仍然无法覆盖到所有情况。经验以及广泛的评价十分重 要，遇到特殊情况时，大家应该咨询有经验的C程序员，或者查看那些经验丰富的C程序员们的代码(最好遵循这些规则)。</p>
<p>本文中的标准本身并不是必需的，但个别机构或团体可能部分或全部采用该标准作为程序验收的一部分。因此，在你的机构中其他人很可能以一种相似的风 格编码。最终，这些标准的目的是提高可移植性，减少维护工作，尤其是提高代码的清晰度。</p>
<p>这里很多风格的选择都有些许武断。混合的编码风格比糟糕的编码风格更难于维护，所以当变更现有代码时，最好是保持与现有代码风格一致，而不是盲目 地遵循本文档中的规则。</p>
<p><font face="Courier New"><i>&quot;清晰的是专业的；不清晰的则是外行的&quot; &#8212; Sir Ernest Gowers </i></font></p>
<p><big><b>2. 文件组织</b></big></p>
<p>一个文件包含的各个部分应该用若干个空行分隔。虽然对源文件没有最大长度限制，但超过1000行的文件处理起来非常不方便。编辑器很可能没有足够 的临时空间来编辑这个文件，编译过程也会因此变得十分缓慢。与回滚到前面所花费的时间相比，那些仅仅呈现了极少量信息的多行星号是不值得的，我们 不鼓励使用。超过79列的行无法被所有的终端都很好地处理，应该尽可能的避免使用。过长的行会导致过深的缩进，这常常是一种代码组织不善的症状。</p>
<p><b>2.1 文件命名惯例</b></p>
<p>文件名由一个基础名、一个可选的句号以及后缀组成。名字的第一个字符应该是一个字母，并且所有字符(除了句号)都应该是小写的字母和数字。基础名 应该由八个或更少的字符组成，后缀应该由三个或更少的字符组成(四个，如果你包含句号的话)。这些规则对程序文件以及程序使用和产生的默认文件都 适用(例如，&quot;rogue.sav&quot;)。</p>
<p>一些编译器和工具要求文件名符合特定的后缀命名约定。下面是后缀命名要求：</p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; C源文件的名字必须以.c结尾<br />
	&nbsp;&nbsp;&nbsp; 汇编源文件的名字必须以.s结尾 </font></p>
<p>我们普遍遵循以下命名约定：</p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; 可重定位目标文件名以.o结尾<br />
	&nbsp;&nbsp;&nbsp; 头文件名以.h结尾<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; 在多语言环境中一个可供选择的更好的约定是用语言类型和.h共同作为后缀(例如，&quot;foo.c.h&quot; 或 &quot;foo.ch&quot;)。</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; Yacc源文件名以.y结尾<br />
	&nbsp;&nbsp;&nbsp; Lex源文件名以.l结尾 </font></p>
<p>C++使用编译器相关的后缀约定，包括.c，..c，.cc，.c.c以及.C。由于大多C代码也是C++代码，因此这里并没有一个明确的方案。</p>
<p>此外，我们一般约定使用&quot;Makefile&quot;(而不是&quot;makefile&quot;)作为make(对于那些支持make的系统)工具的控制文件，并且使 用&quot;README&quot;作为简要描述目录内容或目录树的文件。</p>
<p><b>2.2 程序文件</b></p>
<p>下面是一个程序文件各个组成部分的推荐排列顺序：</p>
<p>文件的第一部分是一个序，用于说明该文件中的内容是什么。对文件中的对象(无论它们是函数，外部数据声明或定义，或是其他一些东西)用途的描述比 一个对象名字列表更加有用。这个序可选择地包含作者信息、修订控制信息以及参考资料等。</p>
<p>接下来是所有被包含的头文件。如果某个头文件被包含的理由不是那么显而易见，我们需要通过增加注释说明原因。大多数情况下，类似stdio.h这 样的系统头文件应该被放在用户自定义头文件的前面。</p>
<p>接下来是那些用于该文件的defines和typedefs。一个常规的顺序是先写常量宏、再写函数宏，最后是typedefs和枚举 (enums)定义。</p>
<p>接下来是全局(外部)数据声明，通常的顺序如下：外部变量，非静态(non-static)全局变量，静态全局变量。如果一组定义被用于部分特定 全局数据（如一个标志字），那么这些定义应该被放在对应数据声明后或嵌入到结构体声明中，并将这些定义缩进到其应用的声明的第一个关键字的下一个 层次(译注：实在没有搞懂后面这句的含义)。</p>
<p>最后是函数，函数应该以一种有意义的顺序排列。相似的函数应该放在一起。与深度优先(函数定义尽可能在他们的调用者前后)相比，我们应该首选广度 优先方法(抽象层次相似的函数放在一起)。这里需要相当多的判断。如果定义大量本质上无关的工具函数，可考虑按字母表顺序排列。</p>
<p><b>2.3 头文件</b></p>
<p>头文件是那些在编译之前由C预处理器包含在其他文件中的文件。诸如stdio.h的一些头文件被定义在系统级别，所有使用标准I/O库的程序必须 包含它们。头文件还用来包含数据声明和定义，这些数据不止一个程序需要。头文件应该按照功能组织，例如，独立子系统的声明应该放到独立的头文件 中。如果一组声明在代码从一种机器移植到另外一种机器时变动的可能性很大，那么这些声明也应该被放在独立的头文件中。</p>
<p>避免私有头文件的名字与标准库头文件的名字一样。下面语句：</p>
<p><font face="Courier New">#include &quot;math.h&quot;</font></p>
<p>当预期的头文件在当前目录下没有找到时，它将会包含标准库中的math头文件。如果这的确是你所期望发生的，那么请加上注释。包含头文件时不要使 用绝对路径。当从标准位置获取头文件时，请使用&lt;name&gt;包含头文件；或相对于当前路径定义它们。C编译器的&quot;include- path&quot;选项(在许多系统中为-l)是处理扩展私有库头文件的最好方法，它允许在不改变源码文件的情况下重新组织目录结构。</p>
<p>声明了函数或外部变量的头文件应该被那些定义了这些函数和变量的文件所包含。这样一来，编译器就可以做类型检查了，并且外部声明将总是与定义保持 一致。</p>
<p>在头文件中定义变量往往是个糟糕的想法，它经常是一个在文件间对代码进行低劣划分的症状。此外，在一次编译中，像typedef和经过初始化的数 据定义无法被编译器看到两次。在一些系统中，重复的没有使用extern关键字修饰的未初始化定义也会导致问题。当头文件嵌套时，会出现重复的声 明，这将导致编译失败。</p>
<p>头文件不应该嵌套。一个头文件的序应该描述其使用的其他被包含的头文件的实用特性。在极特殊情况下，当大量头文件需要被包含在多个不同的源文件中 时，可以被接受的做法是将公共的头文件包含在一个单独的头文件中。</p>
<p>一个通用的做法是将下面这段代码加入到每个头文件中以防止头文件被意外多次包含。</p>
<p><font face="Courier New">#ifndef EXAMPLE_H<br />
	#define EXAMPLE_H<br />
	&nbsp;&#8230;&nbsp;&nbsp;&nbsp; /* body of example.h file */<br />
	#endif /* EXAMPLE_H */</font></p>
<p>我们不应该对这种避免多次包含的机制产生依赖，特别是不应该因此而嵌套包含头文件。</p>
<p><b>2.4 其他文件</b></p>
<p>还有一个惯例就是编写一个名为&quot;README&quot;的文件，用于描述程序的整体情况以及问题。例如，我们经常在README包含程序所使用的条件编译 选项列表以及相关说明，还可以包含机器无关的文件列表等。</p>
<p><big><b>4. 声明</b></big></p>
<p>全局声明应该从第一列开始。在所有外部数据声明的前面都应该放置extern关键字。如果一个外部变量是一个在定义时大小确定的数组，那么这个数 组界限必须在extern声明时显示指出，除非数组的大小与数组本身编码在一起了(例如，一个总是以0结尾的只读字符数组)。重复声明数组大小对 于一些使用他人编写的代码的人特别有益。</p>
<p>指针修饰符*应该与变量名在一起，而不是与类型在一起。</p>
<p><font face="Courier New">char&nbsp; *s, *t, *u;</font></p>
<p>替换</p>
<p><font face="Courier New">char*&nbsp; s, t, u;</font></p>
<p>后者是错误的，因为实际上t和u并未如预期那样被声明为指针。</p>
<p>不相关的声明，即使是相同类型的，也应该独立占据一行。我们应该对声明对象的角色进行注释，不过当常量名本身足以说明角色时，使用#define 定义的常量列表则不需要注释。通常多行变量名、值与注释使用相同缩进，使得他们在一列直线上。尽量使用Tab字符而不是空格。结构体和联合体的声 明时，每个元素应该单独占据一行，并附带一条注释。{应该与结构体的tag名放在同一行，}应该放在声明结尾的第一列。</p>
<p><font face="Courier New">struct boat {<br />
	&nbsp;&nbsp;&nbsp; int&nbsp;&nbsp;&nbsp; wllength;&nbsp;&nbsp;&nbsp; /* water line length in meters */<br />
	&nbsp;&nbsp;&nbsp; int&nbsp;&nbsp;&nbsp; type;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; /* see below */<br />
	&nbsp;&nbsp;&nbsp; long&nbsp;&nbsp; sailarea;&nbsp;&nbsp;&nbsp; /* sail area in square mm */<br />
	};</font></p>
<p><font face="Courier New">/* defines for boat.type */<br />
	#define&nbsp;&nbsp;&nbsp; KETCH&nbsp;&nbsp;&nbsp; (1)<br />
	#define&nbsp;&nbsp;&nbsp; YAWL&nbsp;&nbsp;&nbsp;&nbsp; (2)<br />
	#define&nbsp;&nbsp;&nbsp; SLOOP&nbsp;&nbsp;&nbsp; (3)<br />
	#define&nbsp;&nbsp;&nbsp; SQRIG&nbsp;&nbsp;&nbsp; (4)<br />
	#define&nbsp;&nbsp;&nbsp; MOTOR&nbsp;&nbsp;&nbsp; (5)</font></p>
<p>这些defines有时放在结构体内type声明的后面，并使用足够的tab缩进到结构体成员成员的下一级。如果这些实际值不那么重要的话，使用 enum会更好。</p>
<p><font face="Courier New">enum bt { KETCH=1, YAWL, SLOOP, SQRIG, MOTOR };<br />
	struct boat {<br />
	&nbsp;&nbsp;&nbsp; int&nbsp;&nbsp;&nbsp;&nbsp; wllength;&nbsp;&nbsp;&nbsp; /* water line length in meters */<br />
	&nbsp;&nbsp;&nbsp; enum bt type;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; /* what kind of boat */<br />
	&nbsp;&nbsp;&nbsp; long&nbsp;&nbsp;&nbsp; sailarea;&nbsp;&nbsp;&nbsp; /* sail area in square mm */<br />
	};</font></p>
<p>任何初值重要的变量都应该被显式地初始化，或者至少应该添加注释，说明依赖C的默认初始值0。空初始化&quot;{}&quot;应该永远不被使用。结构体初始化应 该用大括号完全括起来。用于初始化长整型(long)的常量应该使用显式长度。使用大写字母，例如2l看起来更像21，数字二十一。</p>
<p><font face="Courier New">int&nbsp;&nbsp; x = 1;<br />
	char&nbsp; *msg = &quot;message&quot;;<br />
	struct boat&nbsp;&nbsp;&nbsp; winner[] = {<br />
	&nbsp;&nbsp;&nbsp; { 40, YAWL, 6000000L },<br />
	&nbsp;&nbsp;&nbsp; { 28, MOTOR, 0L },<br />
	&nbsp;&nbsp;&nbsp; { 0 },<br />
	};</font></p>
<p>如果一个文件不是独立程序，而是某个工程整体的一部分，那么我们应该最大化的利用static关键字，使得函数和变量对于单个文件来说是局部范畴 的。只有在有清晰需求且无法通过其他方式实现的特殊情况时，我们才允许变量被其他文件访问。这种情况下应该使用注释明确告知使用了其他文件中的变 量；注释应该说明其他文件的名字。如果你的调试器遮蔽了你需要在调试阶段查看的静态对象，那么可以将这些变量声明为STATIC，并根据需要决定 是否#define STATIC。</p>
<p>最重要的类型应该被typedef，即使他们只是整型，因为独立的名字使得程序更加易读(如果只有很少的几个integer的typedef)。 结构体在声明时应该被typedef。保持结构体标志的名字与typedef后的名字相同。</p>
<p><font face="Courier New">typedef struct splodge_t {<br />
	&nbsp;&nbsp;&nbsp; int&nbsp; sp_count;<br />
	&nbsp;&nbsp;&nbsp; char *sp_name, *sp_alias;<br />
	} splodge_t;</font></p>
<p>总是声明函数的返回类型。如果函数原型可用，那就使用它。一个常见的错误就是忽略那些返回double的外部数学函数声明。那样的话，编译器就会 假定这些函数的返回值为一个整型数，并且将bit位逐一尽职尽责的注意转换为一个浮点数(无意义)。</p>
<p><font face="Courier New"><i>&quot;C语言的观点之一是程序员永远是对的&quot; &#8212; Michael DeCorte</i></font></p>
<p><big><b>5. 函数声明</b></big></p>
<p>每个函数前面应该放置一段块注释，概要描述该函数做什么以及(如果不是很清晰)如何使用该函数。重要的设计决策讨论以及副作用说明也适合放在注释 中。避免提供那些代码本身可以清晰提供的信息。</p>
<p>函数的返回类型应该单独占据一行，(可选的)缩进一个级别。不用使用默认返回类型int；如果函数没有返回值，那么将返回类型声明为void。如 果返回值需要大段详细的说明，可以在函数之前的注释中描述；否则可以在同一行中对返回类型进行注释。函数名(以及形式参数列表)应该被单独放在一 行，从第一列开始。目的(返回值)参数一般放在第一个参数位置(从左面开始)。所有形式参数声明、局部声明以及函数体中的代码都应该缩进一级。函 数体的开始括号应该单独一行，放在开始处的第一列。</p>
<p>每个参数都应该被声明(不要使用默认类型int)。通常函数中每个变量的角色都应该被描述清楚，我们可以在函数注释中描述，或如果每个声明单独一 行，我们可以将注释放在同一行上。像循环计数器&quot;i&quot;，字符串指针&quot;s&quot;以及用于标识字符的整数类型&quot;c&quot;这些简单变量都无需注释。如果一组函数 都拥有一个相似的参数或局部变量，那么在所有函数中使用同一个名字来标识这个变量是很有益处的(相反，避免在相关函数中使用一个名字标识用途不同 的变量)。不同函数中的相似参数还应该放在各个参数列表中的相同位置。</p>
<p>参数和局部变量的注释应该统一缩进以排成一列。局部变量声明应用一个空行与函数语句分隔开来。</p>
<p>当你使用或声明变长参数的函数时要小心。目前在C中尚没有真正可移植的方式处理变长参数。最好设计一个使用固定个数参数的接口。如果一定要使用变 长参数，请使用标准库中的宏来声明具有变长参数的函数。</p>
<p>如果函数使用了在文件中没有进行全局声明的外部变量(或函数)，我们应该在函数体内部使用extern关键字单独对这些变量进行声明。</p>
<p>避免局部声明覆盖高级别的声明。尤其是，局部变量不应该在嵌套代码块中被重声明。虽然这在C中是合法的，但是当使用-h选项时，潜在的冲突可能性 足以让lint工具发出抱怨之声。</p>
<p><big><b>6. 空白</b></big></p>
<p><font face="Courier New">int i;main(){for(;i["]<i;++i){--i;}"];read(&#39;-&#39;-&#39;-&#39;,i+++&quot;hell\<br /> o, world!\n&quot;,&#39;/&#39;/&#39;/&#39;));}read(j,i,p){write(j/p+p,i&#8212;j,i/i);}<br />
	<i>- 不光彩的事情，模糊C代码大赛，1984年。作者要求匿名。</i></i;++i){--i;}"];read(&#39;-&#39;-&#39;-&#39;,i+++&quot;hell\<br /></font></p>
<p>通常情况下，请使用纵向和横向的空白。缩进和空格应该反映代码的块结构。例如，在一个函数定义与下一个函数的注释之间，至少应该有两行空白。</p>
<p>如果一个条件分支语句过长，那就应该将它拆分成若干单独的行。</p>
<p><font face="Courier New">if (foo-&gt;next==NULL &amp;&amp; totalcount&lt;needed &amp;&amp; needed&lt;=MAX_ALLOT<br />
	&nbsp;&nbsp;&nbsp; &amp;&amp; server_active(current_input)) { &#8230;</font></p>
<p>也许下面这样更好</p>
<p><font face="Courier New">if (foo-&gt;next == NULL<br />
	&nbsp;&nbsp;&nbsp; &amp;&amp; totalcount &lt; needed &amp;&amp; needed &lt;= MAX_ALLOT<br />
	&nbsp;&nbsp;&nbsp; &amp;&amp; server_active(current_input))<br />
	{<br />
	&nbsp;&nbsp;&nbsp; &#8230;</font></p>
<p>类似地，复杂的循环条件也应该被拆分为不同行。</p>
<p><font face="Courier New">for (curr = *listp, trail = listp;<br />
	&nbsp;&nbsp;&nbsp; curr != NULL;<br />
	&nbsp;&nbsp;&nbsp; trail = &amp;(curr-&gt;next), curr = curr-&gt;next )<br />
	{<br />
	&nbsp;&nbsp;&nbsp; &#8230;</font></p>
<p>其他复杂的表达式，尤其是那些使用了?:操作符的表达式，最好也能拆分成多行。</p>
<p><font face="Courier New">c = (a == b)<br />
	&nbsp;&nbsp;&nbsp; ? d + f(a)<br />
	&nbsp;&nbsp;&nbsp; : f(b) &#8211; d;</font></p>
<p>当关键字后面有放在括号内的表达式时，应该使用空格将关键字与左括号分隔(sizeof操作符是个例外)。在参数列表中，我们也应该使用空格显式 的将各个参数隔开。然而，带有参数的宏定义一定不能在名字与左括号间插入空格，否则C预编译器将无法识别后面的参数列表。</p>
<p><big><b>7. 例子</b></big></p>
<p>&nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <font face="Courier New">/*<br />
	&nbsp;&nbsp;&nbsp;&nbsp; * Determine if the sky is blue by checking that it isn&#39;t night.<br />
	&nbsp;&nbsp;&nbsp;&nbsp; * CAVEAT: Only sometimes right.&nbsp; May return TRUE when the answer<br />
	&nbsp;&nbsp;&nbsp;&nbsp; * is FALSE.&nbsp; Consider clouds, eclipses, short days.<br />
	&nbsp;&nbsp;&nbsp;&nbsp; * NOTE: Uses &#39;hour&#39; from &#39;hightime.c&#39;.&nbsp; Returns &#39;int&#39; for<br />
	&nbsp;&nbsp;&nbsp;&nbsp; * compatibility with the old version.<br />
	&nbsp;&nbsp;&nbsp;&nbsp; */<br />
	&nbsp;&nbsp;&nbsp; int&nbsp; /* true or false */<br />
	&nbsp;&nbsp;&nbsp; skyblue()<br />
	&nbsp;&nbsp;&nbsp; {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; extern int hour;&nbsp;&nbsp; /* current hour of the day */</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return (hour &gt;= MORNING &amp;&amp; hour &lt;= EVENING);<br />
	&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; /*<br />
	&nbsp;&nbsp;&nbsp;&nbsp; * Find the last element in the linked list<br />
	&nbsp;&nbsp;&nbsp;&nbsp; * pointed to by nodep and return a pointer to it.<br />
	&nbsp;&nbsp;&nbsp;&nbsp; * Return NULL if there is no last element.<br />
	&nbsp;&nbsp;&nbsp;&nbsp; */<br />
	&nbsp;&nbsp;&nbsp; node_t *<br />
	&nbsp;&nbsp;&nbsp; tail(nodep)<br />
	&nbsp;&nbsp;&nbsp; node_t&nbsp; *nodep;&nbsp;&nbsp;&nbsp;&nbsp; /* pointer to head of list */<br />
	&nbsp;&nbsp;&nbsp; {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; register node_t *np; /* advances to NULL */<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; register node_t *lp; /* follows one behind np */</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (nodep == NULL)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return (NULL);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; for (np = lp = nodep; np != NULL; lp = np, np = np-&gt;next)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; /* VOID */<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return (lp);<br />
	&nbsp;&nbsp;&nbsp; }</font></p>
<p><big><b>8. 简单语句</b></big></p>
<p>每行只应该有一条语句，除非多条语句关联特别紧密。</p>
<p><font face="Courier New">case FOO:&nbsp; oogle (zork);&nbsp; boogle (zork);&nbsp; break;<br />
	case BAR:&nbsp; oogle (bork);&nbsp; boogle (zork);&nbsp; break;<br />
	case BAZ:&nbsp; oogle (gork);&nbsp; boogle (bork);&nbsp; break;</font></p>
<p>for或while循环语句的空体应该单独放在一行并加上注释，这样可以清晰的看出空体是有意而为，并非遗漏代码。</p>
<p><font face="Courier New">while (*dest++ = *src++)<br />
	&nbsp;&nbsp;&nbsp; ;&nbsp;&nbsp;&nbsp; /* VOID */</font></p>
<p>不要对非零表达式进行默认测试，例如：</p>
<p><font face="Courier New">if (f() != FAIL)</font></p>
<p>比下面的代码更好</p>
<p><font face="Courier New">if (f())</font></p>
<p>即使FAIL的值可能为0(在C中0被认为是假)。当后续有人决定使用-1替代0作为失败返回值时，一个显式的测试将解决你的问题。即使比较的值 永远不会改变，我们也应该使用显式的比较；例如</p>
<p><font face="Courier New">if (!(bufsize % sizeof(int)))</font></p>
<p>应该被写成</p>
<p><font face="Courier New">if ((bufsize % sizeof(int)) == 0)</font></p>
<p>这样可以反映这个测试的数值(非布尔)本质。一个常见的错误点是使用strcmp测试字符串是否相同，这个测试的结果永远不应该被放弃。比较好的 方法是定义一个宏STREQ。</p>
<p><font face="Courier New">#define STREQ(a, b) (strcmp((a), (b)) == 0)</font></p>
<p>对谓词或满足下面约束的表达式，非零测试经常被放弃：</p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; 0表示假，其他都为真。<br />
	&nbsp;&nbsp;&nbsp; 通过其命名可以看出返回真是显而易见的。 </font></p>
<p>用isvalid或valid称呼一个谓词，不要用checkvalid。</p>
<p>一个非常常见的实践就是在一个全局头文件中声明一个布尔类型&quot;bool&quot;。这个特殊的名字可以极大地提高代码可读性。</p>
<p><font face="Courier New">typedef int&nbsp;&nbsp;&nbsp; bool;<br />
	#define FALSE&nbsp;&nbsp;&nbsp; 0<br />
	#define TRUE&nbsp;&nbsp;&nbsp; 1</font></p>
<p>或</p>
<p><font face="Courier New">typedef enum { NO=0, YES } bool;</font></p>
<p>即便有了这些声明，也不要检查一个布尔值与1(TRUE，YES等)的相当性；可用测试与0(FALSE，NO等)的不等性替代。绝大多数函数都 可以保证为假的时候返回0，但为真的时候只返回非零。</p>
<p><font face="Courier New">if (func() == TRUE) { &#8230;</font></p>
<p>必须被写成</p>
<p><font face="Courier New">if (func() != FALSE) { &#8230;</font></p>
<p>如果可能的话，最好为函数/变量重命名或者重写这个表达式，这样就可以显而易见的知道其含义，而无需再与true或false比较了(例如，重命 名为isvalid())。</p>
<p>嵌入赋值语句也有用武之地。在一些结构中，在没有降低代码可读性的前提下，没有比这更好的方式来实现这个结果了。</p>
<p><font face="Courier New">while ((c = getchar()) != EOF) {<br />
	&nbsp;&nbsp;&nbsp; process the character<br />
	}</font></p>
<p>++和&#8211;操作符可算作是赋值语句。这样，为了某些意图，实现带有副作用的功能。使用嵌入赋值语句也可能提高运行时的性能。不过，大家应该在提高 的性能与下降的可维护性之间做好权衡。当在一些人为的地方使用嵌入赋值语句时，这种情况会发生，例如：</p>
<p><font face="Courier New">a = b + c;<br />
	d = a + r;</font></p>
<p>不应该被下面代码替代：</p>
<p><font face="Courier New">d = (a = b + c) + r;</font></p>
<p>即使后者可能节省一个计算周期。在长期运行时，由于优化器渐获成熟，两者的运行时间差距将下降，而两者在维护性方面的差异将提高，因为人类的记忆 会随着时间的流逝而衰退。</p>
<p>在任何结构良好的代码中，goto语句都应该保守地使用。使用goto带来好处最大的地方是从switch、for和while多层嵌套中跳出， 但这样做的需求也暗示了代码的内层结构应该被抽取出来放到一个单独的返回值为成功或失败的函数中。</p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; for (&#8230;) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; while (&#8230;) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8230;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if (disaster)<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; goto error;</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp; &#8230;<br />
	error:<br />
	&nbsp;&nbsp;&nbsp; clean up the mess</font></p>
<p>当需要goto时候，其对应的标签应该被放在单独一行，并且后续的代码缩进一级。使用goto语句时应该增加注释(可能放在代码块的头)以说明它 的功用和目的。continue应该保守地使用，并且尽可能靠近循环的顶部。Break的麻烦比较少。</p>
<p>非原型函数的参数有时需要被显式做类型提升。例如，如果函数期望一个32bit的长整型，但却被传入一个16bit的整型数，可能会导致函数栈不 对齐。指针，整型和浮点值都会发生此问题。</p>
<p><b><big>9. 复合语句</big></b></p>
<p>复合语句是一个由括号括起来的语句列表。有许多种常见的括号格式化方式。如果你有一个本地标准，那请你与本地标准保持一致，或选择一个标准，并持 续地使用它。在编辑别人的代码时，始终使用那些代码中使用的样式。</p>
<p><font face="Courier New">control {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; statement;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; statement;<br />
	}</font></p>
<p>上面的风格被称为&quot;K&amp;R风格&quot;，如果你还没有找到一个自己喜欢的风格，那么可以优先考虑这个风格。在K&amp;R风格中，if- else语句中的else部分以及do-while语句中的while部分应该与结尾大括号在同一行中。而其他大部分风格中，大括号都是单独占据 一行的。</p>
<p>当一个代码块拥有多个标签时，每个标签应该单独放在一行上。必须为C语言的switch语句的fall-through特性(即在代码段与下一个 case语句之前间没有break)增加注释以利于后期更好的维护。最好是lint风格的注释/指示。</p>
<p><font face="Courier New">switch (expr) {<br />
	case ABC:<br />
	case DEF:<br />
	&nbsp;&nbsp;&nbsp;&nbsp; statement;<br />
	&nbsp;&nbsp;&nbsp; break;<br />
	case UVW:<br />
	&nbsp;&nbsp;&nbsp;&nbsp; statement;<br />
	&nbsp;&nbsp;&nbsp;&nbsp; /*FALLTHROUGH*/<br />
	case XYZ:<br />
	&nbsp;&nbsp;&nbsp; statement;<br />
	&nbsp;&nbsp;&nbsp; break;<br />
	}</font></p>
<p>这里，最后那个break是不必要的，但却是必须的，因为如果后续另外一个case添加到最后一个case的后面时，它将阻止fall- through错误的发生。如果使用default case，那么应该该default case放在最后，且不需要break，如果它是最后一个case。</p>
<p>一旦一个if-else语句在if或else段中包含一个复合语句，if和else两个段都应该用括号括上(称为全括号(fully bracketed)语法)。</p>
<p><font face="Courier New">if (expr) {<br />
	&nbsp;&nbsp;&nbsp; statement;<br />
	} else {<br />
	&nbsp;&nbsp;&nbsp; statement;<br />
	&nbsp;&nbsp;&nbsp; statement;<br />
	}</font></p>
<p>在如下面那样的没有第二个else的if-if-else语句序列里，括号也是不必可少的。如果ex1后面的括号被省略，编译器解析将出错：</p>
<p><font face="Courier New">if (ex1) {<br />
	&nbsp;&nbsp;&nbsp; if (ex2) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; funca();<br />
	&nbsp;&nbsp;&nbsp; }<br />
	} else {<br />
	&nbsp;&nbsp;&nbsp; funcb();<br />
	}</font></p>
<p>一个带else if的if-else语句在书写上应该让else条件左对齐。</p>
<p><font face="Courier New">if (STREQ (reply, &quot;yes&quot;)) {<br />
	&nbsp;&nbsp;&nbsp; statements for yes<br />
	&nbsp;&nbsp;&nbsp; &#8230;<br />
	} else if (STREQ (reply, &quot;no&quot;)) {<br />
	&nbsp;&nbsp;&nbsp; &#8230;<br />
	} else if (STREQ (reply, &quot;maybe&quot;)) {<br />
	&nbsp;&nbsp;&nbsp; &#8230;<br />
	} else {<br />
	&nbsp;&nbsp;&nbsp; statements for default<br />
	&nbsp;&nbsp;&nbsp; &#8230;<br />
	}</font></p>
<p>这种格式看起来像一个通用的switch语句，并且缩进反映了在这些候选语句间的精确切换，而不是嵌套的语句。</p>
<p>Do-while循环总是使用括号将循环体括上。</p>
<p>下面的代码非常危险：</p>
<p><font face="Courier New">#ifdef CIRCUIT<br />
	#&nbsp;&nbsp;&nbsp; define CLOSE_CIRCUIT(circno) { close_circ(circno); }<br />
	#else<br />
	#&nbsp;&nbsp;&nbsp; define CLOSE_CIRCUIT(circno)<br />
	#endif</font></p>
<p><font face="Courier New">&#8230;<br />
	if (expr)<br />
	&nbsp;&nbsp;&nbsp; statement;<br />
	else<br />
	&nbsp;&nbsp;&nbsp; CLOSE_CIRCUIT(x)<br />
	++i;</font></p>
<p>注意，在CIRCUIT没有定义的系统上，语句++i仅仅在expr是假的时候获得执行。这个例子指出宏用大写命名的价值，以及让代码完全括号化 的价值。</p>
<p>有些时候，通过break，continue，goto或return，if可以无条件地进行控制转移。else应该是隐式的，并且代码不应该缩 进。</p>
<p><font face="Courier New">if (level &gt; limit)<br />
	&nbsp;&nbsp;&nbsp; return (OVERFLOW)<br />
	normal();<br />
	return (level);</font></p>
<p>平坦的缩进告诉读者布尔测试在密封块的其他部分是保持不变的。</p>
<p><big><b>10. 操作符</b></big></p>
<p>一元操作符不应该与其唯一的操作数分开。通常，所有其他二元操作符都应该使用空白与其操作树分隔开，但&#39;.&#39;和&#39;-&gt;&#39;例外。当遇到复杂表 达式的时候我们需要做出一些判断。如果内层操作符没有使用空白分隔而外层使用了，那么表达式也许会更清晰些。</p>
<p>如果你认为一个表达式很难于阅读，可以考虑将这个表达式拆分为多行。在接近中断点的最低优先级操作符处拆分是最好的选择。由于C具有一些想不到的 优先级规则，混合使用操作符的表达式应该使用括号括上。但是过多的括号也会使得代码可读性变差，因为人类不擅长做括号匹配。</p>
<p>二元逗号操作符也会被使用到，但通常我们应该避免使用它。逗号操作符的最大用途是提供多元初始化或操作，比如在for循环语句中。复杂表达式，例 如那些使用了嵌套三元?:操作符的表达式，可能引起困惑，并且应该尽可能的避免使用。三元操作符和逗号操作符在一些使用宏的地方很有用，诸如 getchar。在三元操作符?:前的逻辑表达式的操作数应该被括起来，并且两个子表达式的返回值应该是相同类型。</p>
<p><b><big>11. 命名约定 </big></b></p>
<p>毫无疑问，每个独立的工程都有一套自己的命名约定，不过仍然有一些通用的规则值得参考。</p>
<p>&nbsp;&nbsp;&nbsp; * 为系统用途保留以下划线开头或下划线结尾的名字，并且这些名字不应该被用在任何用户自定义的名字中。大多数系统使用这些名字用于用户不应 该也不需知道的名字中。如果你一定要使用你自己私有的标识符，可以用标识它们归属的包的字母作为开头。</p>
<p>&nbsp;&nbsp;&nbsp; * #define定义的常量名字应该全部大写。</p>
<p>&nbsp;&nbsp;&nbsp; * Enum常量应该大写或全部大写。</p>
<p>&nbsp;&nbsp;&nbsp; * 函数名、typedef名，变量名以及结构体、联合体与枚举标志的名字应该用小写字母。</p>
<p>&nbsp;&nbsp;&nbsp; * 很多&quot;宏函数&quot;都是全部大写的。一些宏(诸如getchar和putchar)使用小写字母命名，这事因为他们可能被当成函数使用。只有在宏的行为类似一 个函数调用时才允许小写命名的宏，也就是说它们只对其参数进行一次求值，并且不会给具名形式参数赋值。有些时候我们无法编写出一个具有函数行为的 宏，即使其参数也只是求值一次。</p>
<p>&nbsp;&nbsp;&nbsp; * 避免在同一情形下使用不同命名方式，比如foo和Foo。同样避免foobar和foo_bar这种方式。需要考虑这样所带来的困惑。</p>
<p>&nbsp;&nbsp;&nbsp; * 同样，避免使用看起来相似的名字。在很多终端以及打印设备上，&#39;I&#39;、&#39;1&#39;和&#39;l&#39;非常相似。给变量命名为l特别糟糕，因为它看起来十分像常量&#39;1&#39;。</p>
<p>通常，全局名字(包括enum)应该具有一个统一的前缀，通过该前缀名我们可以识别出这个名字归属于哪个模块。全局变量可以选择汇集在一个全局结 构中。typedef的名字通常在结尾加一个&#39;t&#39;。</p>
<p>避免名字与各种标准库中的名字冲突。一些系统可能包含一些你所不需要的库。另外你的程序将来某天很可能也要扩展。</p>
<p><big><b>12. 常量</b></big></p>
<p>数值型常量不应该被硬编码到源文件中。应该使用C预处理器的#define特性为常量赋予一个有意义的名字。符号化的常量可以让代码具有更好的可 读性。在一处地方统一定义这些值也便于进行大型程序的管理，这样常量值可以在一个地方进行统一修改，只需修改define的值即可。枚举数据类型 更适合声明一组具有离散值的变量，并且编译器还可以对其进行额外的类型检查。至少，任何硬编码的值常量必须具有一段注释，以说明该值的来历。</p>
<p>常量的定义应该与其使用是一致的；例如使用540.0作为一个浮点数，而不是使用540外加一个隐式的float类型转换。有些时候常量0和1被 直接使用而没有用define进行定义。例如，一个for循环语句中用于标识数组下标的常量，</p>
<p><font face="Courier New">for (i = 0; i &lt; ARYBOUND; i++)</font></p>
<p>上面代码是合理的，但下面代码</p>
<p><font face="Courier New">door_t *front_door = opens(door[i], 7);<br />
	if (front_door == 0)<br />
	&nbsp;&nbsp;&nbsp; error(&quot;can&#39;t open %s\\\\n&quot;, door[i]);</font></p>
<p>是不合理的。在最后的那个例子中，front_door是一个指针。当一个值是指针的时候，它应该与NULL比较而不是与0比较。NULL被定义 在标准I/O库头文件stdio.h中，在一些新系统中它在stdlib.h中定义。即使像1或0这样的简单值，我们最好也用define定义成 TRUE和FALSE定义后再使用(有些时候，使用YES和NO可读性更好)。</p>
<p>简单字符常量应该被定义成字面值，不应该使用数字。不鼓励使用非可见文本字符，因为它们是不可移植的。如果非可见文本字符十分必要，尤其是当它们 在字符串中使用时，它们应该定义成三个八进制数字的转义字符(例如： &#39;\007&lsquo;)而非一个字符。即使这样，这种用法也应该考虑其机器相关性，并按这里的方法处理。</p>
<p><b>13. 宏</b></p>
<p>复杂表达式可能会被用作宏参数，这可能会因操作符优先级顺序而引发问题，除非宏定义中所有参数出现的位置都用括号括上了。对这种因参数内副作用而 引发的问题，我们似乎也无能为例，除了在编写表达式时杜绝副作用(无论如何，这都是一个很好的主意)。如果可能的话，尽量在宏定义中对宏参数只进 行一次求值。有很多时候我们无法写出一个可像函数一样使用的宏。</p>
<p>一些宏也当成函数使用(例如，getc和fgetc)。这些宏会被用于实现其他函数，这样一旦宏自身发生变化，使用该宏的函数也会受到影响。在交 换宏和函数时务必要小心，因为函数参数是按值传递的，而宏参数则是通过名称替换。只有在宏定义时特别谨慎小心，才有可能减少使用宏时的担心。</p>
<p>宏定义中应该避免使用全局变量，因为全局变量的名字很可能被局部声明遮盖。对于那些对具名参数进行修改(不是这些参数所指向的存储区域)或被用作 赋值语句左值的宏，我们应该添加相应的注释以给予提醒。那些不带参数但引用变量，或过长或作为函数别名的宏应该使用空参数列表，例如：</p>
<p><font face="Courier New">#define&nbsp;&nbsp;&nbsp; OFF_A()&nbsp;&nbsp;&nbsp; (a_global+OFFSET)<br />
	#define&nbsp;&nbsp;&nbsp; BORK()&nbsp;&nbsp;&nbsp; (zork())<br />
	#define&nbsp;&nbsp;&nbsp; SP3()&nbsp;&nbsp;&nbsp; if (b) { int x; av = f (&amp;x); bv += x; }</font></p>
<p>宏节省了函数调用和返回的额外开销，但当一个宏过长时，函数调用和返回的额外开销就变得微不足道了，这种情况下我们应该使用函数。</p>
<p>在一些情况下，让编译器确保宏在使用时应该以分号结尾是很有必要的。</p>
<p><font face="Courier New">if (x==3)<br />
	&nbsp;&nbsp;&nbsp; SP3();<br />
	else<br />
	&nbsp;&nbsp;&nbsp; BORK();</font></p>
<p>如果省略SP3调用后面的分号，后面的else将会匹配到SP3宏中的那个if。有了分号，else分支就不会与任何if匹配。SP3宏可以这样 安全地实现：</p>
<p><font face="Courier New">#define SP3() \\\\<br />
	&nbsp;&nbsp;&nbsp;&nbsp; do { if (b) { int x; av = f (&amp;x); bv += x; }} while (0)</font></p>
<p>手工给宏定以加上do-while包围看起来很别扭，而且很多编译器和工具会抱怨在while条件是一个常量值。一个用来声明语句的宏可以使得编 码更加容易：</p>
<p><font face="Courier New">#ifdef lint<br />
	&nbsp;&nbsp;&nbsp; static int ZERO;<br />
	#else<br />
	#&nbsp;&nbsp;&nbsp; define ZERO 0<br />
	#endif<br />
	#define STMT( stuff ) do { stuff } while (ZERO)</font></p>
<p>我们可以用下面代码来声明SP3宏：</p>
<p><font face="Courier New">#define SP3() \\\\<br />
	&nbsp;&nbsp;&nbsp; STMT( if (b) { int x; av = f (&amp;x); bv += x; } )</font></p>
<p>使用STMT宏可以有效阻止一些可以潜在改变程序行为的打印排版错误。</p>
<p>除了类型转换、sizeof以及上面那些技巧和手法，只有当整个宏用括号括上时才应该包含关键字。</p>
<p><big><b>14. </b></big><tt><big><b>条件编译</b></big></tt></p>
<p><tt>条件编译在处理机器依赖、调试以及编译阶段设定特定选项时十分有用。不过要小心条件编译。各种控制很容易以一种无法预料的方式结合在一起。如果使 用#ifdef判断机器依赖，请确保当没有机器类型适配时，返回一个错误，而不是使用默认机器类型(使用#error并缩进一级，这样它可以一些老旧的编 译器下工作)。如果你#ifdef优化选项，默认情况下应该是一个未经优化的代码，而不是一个不兼容的程序。确保测试的是未经优化的代码。</tt></p>
<p><tt>注意在#ifdef区域内的文本可能会被编译器扫描(处理)，即使#ifdef求值的结果为假。但即使文件的#ifdef部分永远不能被编译到(例如，#ifdef COMMENT)，这部分也不该随意的放置文本。</tt></p>
<p><tt>尽可能地将#ifdefs放在头文件中，而不是源文件中。使用#ifdef定义可以在源码中统一使用的宏。例如，一个用于检查内存分配的头文件可能这样实现：(省略了REALLOC和FREE)：</tt></p>
<p><font face="Courier New">#ifdef DEBUG<br />
	&nbsp;&nbsp;&nbsp; extern void *mm_malloc();<br />
	#&nbsp;&nbsp;&nbsp; define MALLOC(size) (mm_malloc(size))<br />
	#else<br />
	&nbsp;&nbsp;&nbsp; extern void *malloc();<br />
	#&nbsp;&nbsp;&nbsp; define MALLOC(size) (malloc(size))<br />
	#endif</font></p>
<p><tt>条件编译通常应该基于一个接一个的特性的。多数情况下，都应该避免使用机器或操作系统依赖。</tt></p>
<p><font face="Courier New">#ifdef BSD4<br />
	&nbsp;&nbsp;&nbsp; long t = time ((long *)NULL);<br />
	#endif</font></p>
<p><tt>上面代码之所以糟糕有两个原因：很可能在某个4BSD系统上有更好的选择，并且也可能存在在某个非4BSD系统中上述代码是最佳代码。我们可以通过定义诸 如TIME_LONG和TIME_STRUCTD等宏作为替代，并且在诸如config.h的配置文件中定义一个合适的宏。</tt></p>
<p><big><b>16. 可移植性</b></big></p>
<p><font face="Courier New"><i>&nbsp;&nbsp;&nbsp; &quot;C语言结合了汇编的强大功能和可移植性&quot; &#8212; 无名氏，暗指比尔.萨克。</i></font></p>
<p>可移植代码的好处是有目共睹的。这一节将阐述一些编写可移植代码的指导原则。这里&quot;可移植的&quot;是指一个源码文件能够在不同机器上被编译和执行，其 前提仅仅是在不同平台上可能包含不同的头文件，使用不同的编译器开关选项罢了。头文件包含的#define和typedef可能因机器而异。一般 来说，一个新&quot;机器&quot;是指一种不同的硬件，一种不同的操作系统，一个不同的编译器，或者是这些的任意组合。参考1包含了很多关于风格和可移植 性方面的有用信息。下面是一个隐患列表，当你设计可移植代码时应该考虑避免这些隐患：</p>
<p>&nbsp;&nbsp;&nbsp; * 编写可移植的代码。只有当被证明是必要的情况下才考虑优化的细节。优化后的代码往往是模糊不清、难以理解的。在一台机器上经过优化后的代码，在其他机器上 可能变得更加糟糕。将采用的性能优化手段记录下来并尽可能多地本地化。文档应该解释这些手段的工作原理以及引入它们的原因（例如：&quot;循环执行了无 数次&quot;）</p>
<p>&nbsp;&nbsp;&nbsp; * 要意识到很多东西天生就是不可移植的。比如处理类似程序状态字这样的特定硬件寄存器的代码，以及被设计用于支持某特定硬件部件的代码，诸如汇编器以及 I/O驱动。即使在这种情况下，许多例程和数据仍然可以被设计成机器无关的。</p>
<p>&nbsp;&nbsp;&nbsp; * 组织源文件时将机器无关与机器相关的代码分别放在不同文件中。之后如果这个程序需要被移植到一个新机器上时，我们就可以很容易判断出来哪些需要被改变。为 一些文件的头文件中机器依赖相关的代码添加注释。</p>
<p>&nbsp;&nbsp;&nbsp; * 任何&quot;实现相关&quot;的行为都应该作为机器(编译器)依赖对待。假设编译器或硬件以一种十分古怪的方式实现它。</p>
<p>&nbsp;&nbsp;&nbsp; * 注意机器字长。对象的大小可能不直观，指针大小也不总是与整型大小相同，也不总是彼此大小相同，或者可相互自由转换。下面的表中列举了C语言基本类型在不 同机器和编译器下的大小(以bit为单位)。</p>
<p><font face="Courier New">type&nbsp;&nbsp;&nbsp; pdp11&nbsp; VAX/11 68000&nbsp; Cray-2 Unisys Harris 80386<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; series&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; family&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 1100&nbsp;&nbsp; H800<br />
	char&nbsp;&nbsp;&nbsp;&nbsp; 8&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 8&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 8&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 8&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 9&nbsp;&nbsp;&nbsp;&nbsp; 8&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 8<br />
	short&nbsp;&nbsp;&nbsp; 16&nbsp;&nbsp;&nbsp; 16&nbsp;&nbsp;&nbsp;&nbsp; 8/16&nbsp;&nbsp; 64(32)&nbsp;&nbsp; 18&nbsp;&nbsp;&nbsp; 24&nbsp;&nbsp;&nbsp; 8/16<br />
	int&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 16&nbsp;&nbsp;&nbsp; 32&nbsp;&nbsp;&nbsp;&nbsp; 16/32&nbsp; 64(32)&nbsp;&nbsp; 36&nbsp;&nbsp;&nbsp; 24&nbsp;&nbsp;&nbsp; 16/32<br />
	long&nbsp;&nbsp;&nbsp;&nbsp; 32&nbsp;&nbsp;&nbsp; 32&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 32&nbsp;&nbsp;&nbsp; 64&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 36&nbsp;&nbsp;&nbsp; 48&nbsp;&nbsp;&nbsp; 32<br />
	char*&nbsp;&nbsp;&nbsp; 16&nbsp;&nbsp;&nbsp; 32&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 32&nbsp;&nbsp;&nbsp; 64&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 72&nbsp;&nbsp;&nbsp; 24&nbsp;&nbsp;&nbsp; 16/32/48<br />
	int*&nbsp;&nbsp;&nbsp;&nbsp; 16&nbsp;&nbsp;&nbsp; 32&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 32&nbsp;&nbsp;&nbsp; 64(24)&nbsp;&nbsp; 72&nbsp;&nbsp;&nbsp; 24&nbsp;&nbsp;&nbsp; 16/32/48<br />
	int(*)() 16&nbsp;&nbsp;&nbsp; 32&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 32&nbsp;&nbsp;&nbsp; 64&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 576&nbsp;&nbsp; 24&nbsp;&nbsp;&nbsp; 16/32/48</font></p>
<p>有些机器针对某一类型可能有不止一个大小。其类型大小取决于编译器和不同的编译期标志。下面表展示了大多数系统的&quot;安全&quot;类型大小。无符号与带符 号数具有相同的大小(单位:bit)。</p>
<p><font face="Courier New">Type&nbsp;&nbsp;&nbsp; Minimum&nbsp; No Smaller<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; # Bits&nbsp;&nbsp; Than<br />
	char&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 8&nbsp;&nbsp;<br />
	short&nbsp;&nbsp; &nbsp;&nbsp; 16&nbsp;&nbsp;&nbsp; char<br />
	int&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 16&nbsp;&nbsp;&nbsp; short<br />
	long&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 32&nbsp;&nbsp;&nbsp; int<br />
	float&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 24&nbsp;&nbsp;<br />
	double&nbsp;&nbsp;&nbsp;&nbsp; 38&nbsp;&nbsp;&nbsp; float<br />
	any *&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 14&nbsp;&nbsp;<br />
	char *&nbsp;&nbsp;&nbsp;&nbsp; 15&nbsp;&nbsp;&nbsp; any *<br />
	void *&nbsp;&nbsp;&nbsp;&nbsp; 15&nbsp;&nbsp;&nbsp; any *</font></p>
<p>&nbsp;&nbsp;&nbsp; * void类型可以保证有足够位精度来表示一个指向任意数据对象的指针。void()()类型可以保证表示一个指向任意函数的指针。当你需要通用指针时 可以使用这些类型(在一些旧的编译器里，分别用char和char()()表示)。确保在使用这些指针类型之前将其转换回正确的类型。</p>
<p>&nbsp;&nbsp;&nbsp; * 即使说一个int和一个char类型大小相同，它们仍可能具有不同的格式。例如，下面例子在一些sizeof(int)等于 sizeof(char)的机器上可能失败。其原因在与free函数期望一个char，但却传入了一个int。</p>
<p>&nbsp;&nbsp;&nbsp;<font face="Courier New"> int *p = (int *) malloc (sizeof(int));<br />
	&nbsp;&nbsp; free (p);</font></p>
<p>&nbsp;&nbsp;&nbsp; * 注意，一个对象的大小不能保证这个对象的精度。Cray-2可能使用64位来存储一个整型，但一个长整型转换为一个整型并且再转换回长整型后可能会被截断 为32位。</p>
<p>&nbsp;&nbsp;&nbsp; * 整型常量0可以强制转型为任何指针类型。转换后的指针称为对应那个类型的空指针，并且与那个类型的其他指针不同。空指针比较总是与常量0相当。空指针不应 该与一个值为0的变量比较。空指针不总是使用全0的位模式表示。两个不同类型的空指针有些时候可能不同。某个类型的空指针被强制转换为另外一个类 型的指针，其结果是该指针转换为第二个类型的空指针。</p>
<p>&nbsp;&nbsp;&nbsp; * 对于ANSI编译器，当两个类型相同的指针访问同一块存储区时，则它们比较是相等的。当一个非0整型常量被转换为指针类型时，它们可能与其他指针相等。对 于非ANSI编译器，访问同一块存储区的两个指针比较可能并不相同。例如，下面两个指针比较可能相等或不相等，并且他们可能或可能没有访问同一块 存储区域。</p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; ((int *) 2 )<br />
	&nbsp;&nbsp;&nbsp; ((int *) 3 )</font></p>
<p>如果你需要&#39;magic&#39;指针而不是NULL，要么分配一些内存，要么将指针视为机器相关的。</p>
<p><font face="Courier New">extern int x_int_dummy;&nbsp;&nbsp;&nbsp; /* in x.c */<br />
	#define X_FAIL&nbsp;&nbsp;&nbsp; (NULL)<br />
	#define X_BUSY&nbsp;&nbsp;&nbsp; (&amp;x_int_dummy)<br />
	#define X_FAIL&nbsp;&nbsp;&nbsp; (NULL)<br />
	#define X_BUSY&nbsp;&nbsp;&nbsp; MD_PTR1&nbsp; /* MD_PTR1 from &quot;machdep.h&quot; */</font></p>
<p>&nbsp;&nbsp;&nbsp; * 浮点数字既包含精度也包含范围。这些都是数据对象大小无关的。但是，一个32位浮点数在不同机器上溢出时的值有所不同。同时，4.9乘以5.1在不同的机 器上可能产生两个不同的数字。在圆整(rounding)和截断方面的差异将给出特别不同的答案。</p>
<p>&nbsp;&nbsp;&nbsp; * 在一些机器上，一个双精度浮点数在精度或范围方面可能比一个单精度浮点数还要低。</p>
<p>&nbsp;&nbsp;&nbsp; * 在一些机器上，double值的前半部分可能是一个具有相同值的float类型。千万不要依赖于此。</p>
<p>&nbsp;&nbsp;&nbsp; * 提防带符号字符。例如，在某些VAX系统上，用在表达式中的字符是符号扩展的，但在其他一些机器上并非如此。对有符号和无符号有依赖的代码是不可移植的。 例如，如果假设c是正值，arrayc在c为有符号且为负值时将无法正常工作。如果你一定要假设signed或unsigned字符的话，请 用SIGNED或UNSIGNED为其加上注释。无符号字符的行为可由unsigned char保证。</p>
<p>&nbsp;&nbsp;&nbsp; * 避免对ASCII做假设。如果你必须假设，那么请将其记录下来并本地化。请记住字符很可能用不止8位表示。</p>
<p>&nbsp;&nbsp;&nbsp; * 大多数机器采用2的补码表示数，但我们在代码中不应该利用这一特点。使用等价移位操作替代算术运算的优化尤其值得怀疑。如果必须这么做，那么机器相关的代 码应该用#ifdef定义，或者操作应该在#ifdef宏判定下执行。你应该衡量一下使用这种难以理解的代码所节省的时间与做代码移植时找bug 所花费的时间相比孰多孰少。</p>
<p>&nbsp;&nbsp;&nbsp; * 一般情况下，如果字长或值范围非常重要，应该使用typedef定义具有特定大小的类型。大型程序应该具有一个统一的头文件用于提供通用的、大小 (size)敏感的类型的typedef定义，这样更加便于修改以及在紧急修复时查找大小敏感的代码。无符号类型比有符号整型更加编译器无关。如 果既可以用16bit也可以用32bit标识一个简单for循环的计数器，我们应该使用int。因为对于当前机器来说，通过整型可以获取更高效 (自然)的存储单元。</p>
<p>&nbsp;&nbsp;&nbsp; * 数据对齐也很重要。例如，在不同的机器上，一个四字节的整型数的可能以任意地址作为起始地址，也可能只允许以偶数地址作为起始地址，或者只能以4的整数倍 的地址作为起始地址。因此，一个特定的结构体的各个元素在不同的机器上的偏移量有不同，即使给定的这些元素在所有机器上的大小相同。事实上，一个 包含一个32位指针和一个8位字符的结构提在三个不同的机器上可能有三个不同的大小。作为一个推论，对象指针可能无法自由互换；通过一个指向起始 地址为奇数地址长度为4个字节的指针保存一个整型数有时可以正常工作，但有时则会导致产生core，有些时候静悄悄地失败了(在这个过程中会破坏 其他数据)。在那些不按字节寻址的机器上，字符指针更是&quot;事故高发地区&quot;。对齐考虑以及加载器的特殊性使得很容易轻率地认为两个连续声明的变量在 内存中也是连在一起的，或者某个类型的变量已经被适当对齐并可以用作其他类型变量使用了。</p>
<p>&nbsp;&nbsp;&nbsp; * 在一些机器上，诸如VAX(小端)，一个字的字节随着地址的增加，其重要性提高；而另外一些机器上，诸如68000(大端)，随着地址的增加，其重要性降 低。字或更大数据对象(诸如一个双精度字)的字节顺序可能并不相同。因此，任何依赖对象内从左到右方向位模式的代码都值得特别细致的审查。只有当 结构体中两个不同的位字段不被连接以及不被当作一个单元时，这些位字段才具备可移植性。事实上，连接任意两个变量都是不可移植的行为。</p>
<p>&nbsp;&nbsp;&nbsp; * 结构体中有一些未使用的空洞。猜想联合体用于类型欺骗。尤其是，一个值不应该在存储时使用一个类型，而在读取时使用另外一种类型。对联合体来说，一个显式 的标签(tag)字段可能会很有用。</p>
<p>&nbsp;&nbsp;&nbsp; * 不同的编译器在返回结构体时使用不同的约定。这就会导致代码在接受从不同编译器编译的库代码中返回的结构体值时会出现错误。结构体指针不是问题。</p>
<p>&nbsp;&nbsp;&nbsp; * 不要假设参数传递机制。特别是指针大小以及参数求值顺序，大小等。例如，下面的代码就不具备可移植性。</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <font face="Courier New">c = foo (getchar(), getchar());</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; char<br />
	&nbsp;&nbsp;&nbsp; foo (c1, c2, c3)<br />
	&nbsp;&nbsp;&nbsp; char c1, c2, c3;<br />
	&nbsp;&nbsp;&nbsp; {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; char bar = *(&amp;c1 + 1);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return (bar);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; /* often won&#39;t return c2 */<br />
	&nbsp;&nbsp;&nbsp; }</font></p>
<p>&nbsp;&nbsp;&nbsp; * 上面的例子有诸多问题。栈可能向上增长，也可能向下增长(事实上，甚至都不需要一个栈)。参数在传入时可能被扩大，例如一个char可能以int型被传 入。参数可能以从左到右，从右到左，或以任意顺序压入栈，或直接放在寄存器中(根本无需压栈)。参数求值的顺序也可能与压栈的次序有所不同。一个 编译器可能使用多种(不兼容的)调用约定。</p>
<p>&nbsp;&nbsp;&nbsp; * 在某些机器上，空字符指针((char *)0)常被当作指向空字符串的指针对待。不要依赖于此。</p>
<p>&nbsp;&nbsp; *&nbsp; 不要修改字符串常量。下面就是一个臭名昭著的例子</p>
<p>&nbsp;&nbsp;&nbsp; <font face="Courier New">s = &quot;/dev/tty??&quot;;<br />
	&nbsp; strcpy (&amp;s[8], ttychars);</font></p>
<p>&nbsp;&nbsp;&nbsp; * 地址空间可能有空洞。简单计算一个数组中未分配空间的元素(在数组实际存储区域之前或之后)的地址可能会导致程序崩溃。如果这个地址被用于比较，有时程序 可以运行，但会破坏数据，报错，或陷入死循环。在ANSI C中，指向一个对象数组的指针指向数组结尾后的第一个元素是合法的，这在一些老编译器上通常是安全的。不过这个&quot;在外边&quot;不可以被解引用。</p>
<p>&nbsp;&nbsp;&nbsp; * 只有==和!=比较可用于某给定类型的所有指针。当两个指针指向同一个数组内的元素(或数组后第一个元素)时，使用&lt;&lt;、&lt;=、&amp; amp; gt;或&gt;=对两个指针进行比较是可移植的。同样，仅仅对指向同一个数组内的元素(或数组后第一个元素)的两个指针使用算术操作符才是可移 植的。</p>
<p>&nbsp;&nbsp;&nbsp; * 字长(word size)也影响移位和掩码。下面代码在一些68000机器上只会将一个整型数的最右三个位清0，而在其他机器上它还会将高地址的两个字节清零。x &amp;= 0177770 使用 x &amp;= ~07可以在所有机器上正常工作。位字段(bitfield)没有这些问题。</p>
<p>&nbsp;&nbsp;&nbsp; * 表达式内的副作用可能导致代码语义是编译器相关的，因为在大多数情况下C语言的求值顺序是没有显式定义的。下面是一个臭名昭著的例子：</p>
<p>&nbsp;&nbsp;&nbsp; <font face="Courier New">a[i] = b[i++];</font></p>
<p>&nbsp;&nbsp;&nbsp; 在上面的例子中，我们只知道b的下标值没有被增加。a的下标i值可能是自增后的值也可能是自增前的值。</p>
<p>&nbsp;&nbsp;&nbsp; <font face="Courier New">struct bar_t { struct bar_t *next; } bar;<br />
	&nbsp;&nbsp;&nbsp; bar-&gt;next = bar = tmp;</font></p>
<p>在第二个例子中，bar-&gt;next的地址很可能在bar被赋值之前被计算使用。</p>
<p><font face="Courier New">bar = bar-&gt;next = tmp;</font></p>
<p>第三个例子中，bar可能在bar-&gt;next之前被赋值。虽然这可能有悖于&quot;赋值从右到左处理&quot;的规则，但这确是一个合法的解析。考虑下 面的例子：</p>
<p><font face="Courier New">long i;<br />
	short a[N];<br />
	i = old<br />
	i = a[i] = new;</font></p>
<p>赋给i的值必须是一个按照从右到左的处理顺序进行赋值处理后的值。但是i可能在ai被赋值前而被赋值为&quot;(long) (short)new&quot;。不同编译器作法不同。</p>
<p>&nbsp;&nbsp;&nbsp; * 质疑代码中出现的数值(&ldquo;魔数&rdquo;)。</p>
<p>&nbsp;&nbsp;&nbsp; * 避免使用预处理器技巧。一些诸如使用/ /粘和字符串以及依赖参数字符串展开的宏会破坏代码可靠性。</p>
<p>&nbsp;&nbsp;&nbsp; <font face="Courier New">#define FOO(string)&nbsp;&nbsp;&nbsp; (printf(&quot;string = %s&quot;,(string)))<br />
	&nbsp;&nbsp;&nbsp; &#8230;<br />
	&nbsp; FOO(filename);</font></p>
<p>只是在有些时候会扩展为</p>
<p><font face="Courier New">&nbsp;(printf(&quot;filename = %s&quot;,(filename)))</font></p>
<p>小心。诡异的预处理器在一些机器上可能导致宏异常中断。下面是一个宏的两种不同实现版本：</p>
<p>&nbsp;<font face="Courier New"> #define LOOKUP(chr)&nbsp;&nbsp;&nbsp; (a['c'+(chr)])&nbsp;&nbsp;&nbsp; /* Works as intended. */<br />
	&nbsp; #define LOOKUP(c)&nbsp;&nbsp;&nbsp; (a['c'+(c)])&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; /* Sometimes breaks. */</font></p>
<p>第二个版本的LOOKUP可能以两种不同的方式扩展，并且会导致代码异常中断。</p>
<p>&nbsp;&nbsp;&nbsp; * 熟悉现有的库函数和定义(但不用太熟悉。与其外部接口相反，库基础设施的内部细节常会改变并且没有警告，这些细节常常也是不可移植的)。你不应该再自己重 新编写字符串比较例程、终端控制例程或为系统结构编写你自己的定义。自己动手实现既浪费你的时间，又使得你的代码可读性变差，因为另外一个读者需 要知道你是否在新的实现中做了什么特殊的事情，并尝试证实它们的存在。同时这样做会使得你无法充分利用一些辅助的微代码或其他有助于提高系统例程 性能的方法。更进一步，它将是一个bug的高产源头。如果可能的话，要知道公共库之间的差异(如ANSI、POSIX等等)。</p>
<p>&nbsp;&nbsp;&nbsp; * 如果lint可用，请使用lint。这个工具对于查找代码中机器相关的构造、其他不一致性以及顺利通过编译器检查的程序bug时具有很高价值。如果你的编 译器具备打开警告的开关，请打开它。</p>
<p>&nbsp;&nbsp;&nbsp; * 质疑在代码块内部的与代码块外部switch或goto有关联的标签(Label)。</p>
<p>&nbsp;&nbsp;&nbsp; 无论类型在哪里，参数都应该被转换为适当的类型。当NULL用在没有原型的函数调用时，请对NULL进行转换。不要让函数调用成为类型欺骗发生的地方。C 语言的类型提升规则很是让人费解，所以尽量小心。例如，如果一个函数接受一个32位长的长整型做为参数，但实际传入的却是一个16位长的整型数， 函数栈可能会无法对齐，这个值也可能会被错误提升。</p>
<p>&nbsp;&nbsp;&nbsp; * 在混用有符号和无符号值的算术计算时请使用显式类型转换</p>
<p>&nbsp;&nbsp;&nbsp; * 应该谨慎使用跨程序的goto、longjmp。很多实现&quot;忘记&quot;恢复寄存器中的值了。尽可能将关键的值声明为volatile，或将它们注释为 VOLATILE。</p>
<p>&nbsp;&nbsp;&nbsp; * 一些链接器将名字转换为小写，并且一些链接器只识别前六个字母作为唯一标识。在这些系统上程序可能会悄悄地中断运行。</p>
<p>&nbsp;&nbsp;&nbsp; * 当心编译器扩展。如果使用了编译器扩展，请将他们视为机器依赖并用文档记录下来。</p>
<p>&nbsp;&nbsp;&nbsp; * 通常程序无法在数据段执行代码或者无法将数据写入代码段。即使程序可以这么做，也无法保证这么做是可靠的。</p>
<p><b><big>17. 标准C</big></b></p>
<p>现代C编译器支持一些或全部的ANSI提议的标准C。无论何时可能的话，尽量用标准C编写和运行程序，并且使用诸如函数原型，常量存储以及 volatile(易失性)存储等特性。标准C通过给优化器提供有有效的信息以提升程序的性能。标准C通过保证所有编译器接受同样的输入语言以及提供相关 机制隐藏机器相关内容或对于那些机器相关代码提供警告的方式提升代码的可移植性。</p>
<p><b>17.1 兼容性</b></p>
<p>编写很容易移植到老编译器上的代码。例如，有条件地在global.h中定义一些新(标准中的)关键字，比如const和volatile。标准编译器预 定义了预处理器符号STDC(见脚注8)。void类型很难简单地处理正确，因为很多老编译器只理解void，但不认识void。最简单的方法就是定义一 个新类型VOIDP(与机器和编译器相关)，通常在老编译器下该类型被定义为char*。</p>
<p><font face="Courier New">#if __STDC__<br />
	&nbsp;&nbsp;&nbsp; typedef void *voidp;<br />
	#&nbsp;&nbsp; define COMPILER_SELECTED<br />
	#endif<br />
	#ifdef A_TARGET<br />
	#&nbsp;&nbsp;&nbsp; define const<br />
	#&nbsp;&nbsp;&nbsp; define volatile<br />
	#&nbsp;&nbsp;&nbsp; define void int<br />
	&nbsp;&nbsp;&nbsp; typedef char *voidp;<br />
	#&nbsp;&nbsp;&nbsp; define COMPILER_SELECTED<br />
	#endif<br />
	#ifdef &#8230;<br />
	&nbsp;&nbsp;&nbsp; &#8230;<br />
	#endif<br />
	#ifdef COMPILER_SELECTED<br />
	#&nbsp;&nbsp;&nbsp; undef COMPILER_SELECTED<br />
	#else<br />
	&nbsp;&nbsp;&nbsp; { NO TARGET SELECTED! }<br />
	#endif</font></p>
<p>注意在ANSI C中，#必须是同一行中预处理器指示符的第一个非空白字符。在一些老编译器中，它必须是同一行中的第一个字符。</p>
<p>当一个静态函数具有前置声明时，前置声明必须包含存储修饰符。在一些老编译器中，这个修饰符必须是&quot;extern&quot;。对于ANSI编译器，这个存储修饰符 必须为static，但全局函数依然必须声明为extern。因此，静态函数的前置声明应该使用一个#define，例如FWD_STATIC，并通 过#ifdef适当定义。</p>
<p>一个&quot;#ifdef NAME&quot;应该要么以&quot;#endif&quot;结尾，要么以&quot;#endif / NAME /结尾，不应该用&quot;#endif NAME&quot;结尾。对于短小的#ifdef不应该使用注释，因为通过代码我们可以明确其含义。</p>
<p>ANSI的三字符组可能导致内容包含??的字符串的程序神秘的中断。</p>
<p><b>17.2 格式化</b></p>
<p>ANSI C的代码风格与常规C一样，但有两点意外：存储修饰符(storage qualifiers)和参数列表。</p>
<p>由于const和volatile的绑定规则很奇怪，因此每个const或volatile对象都应该单独声明。</p>
<p><font face="Courier New">int const *s;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; /* YES */<br />
	int const *s, *t;&nbsp;&nbsp;&nbsp; /* NO */</font></p>
<p>具备原型的函数将参数声明和定义归并在一个参数列表中了。应该在函数的注释中提供各个参数的注释。</p>
<p><font face="Courier New">/*<br />
	&nbsp;* `bp&#39;: boat trying to get in.<br />
	&nbsp;* `stall&#39;: a list of stalls, never NULL.<br />
	&nbsp;* returns stall number, 0 =&gt; no room.<br />
	&nbsp;*/<br />
	int<br />
	enter_pier (boat_t const *bp, stall_t *stall)<br />
	{<br />
	&nbsp;&nbsp;&nbsp; &#8230;</font></p>
<p><b>17.3 原型</b></p>
<p>应该使用函数原型使得代码更加健壮并且运行时性能更好。不幸地是原型的声明</p>
<p><font face="Courier New">extern void bork (char c);</font></p>
<p>与定义不兼容。</p>
<p><font face="Courier New">void<br />
	bork (c)<br />
	char c;<br />
	&nbsp;&#8230;</font></p>
<p>原型中c应该以机器上最自然的类型传入，很可能是一个字节。而非原型化(向后兼容)的定义暗示c总是以一个整型传入。如果一个函数具有可类型提升的参数， 那么调用者和被调用者必须以相等地方式编译。要么都必须使用函数原型，要么都不使用原型。如果在程序设计时参数就是可以提升类型的，那么问题就可以被避 免，例如bork可以定义成接受一个整型参数。</p>
<p>如果定义也是原型化的，上面的声明将工作正常。</p>
<p><font face="Courier New">void<br />
	bork (char c)<br />
	{<br />
	&nbsp;&nbsp;&nbsp; &#8230;</font></p>
<p>不幸地是，原型化的语法将导致非ANSI编译器拒绝这个程序。</p>
<p>但我们可以很容易地通过编写外部声明来同时适应原型与老编译器。</p>
<p><font face="Courier New">#if __STDC__<br />
	#&nbsp;&nbsp;&nbsp; define PROTO(x) x<br />
	#else<br />
	#&nbsp;&nbsp;&nbsp; define PROTO(x) ()<br />
	#endif</font></p>
<p><font face="Courier New">extern char **ncopies PROTO((char *s, short times));</font></p>
<p>注意PROTO必须使用双层括号。</p>
<p>最后，最好只使用一种风格编写代码(例如，使用原型)。当需要非原型化的版本时，可使用一个自动转换工具生成。</p>
<p><b>17.4 Pragmas</b></p>
<p>Pragmas用于以一种可控的方式引入机器相关的代码。很显然，pragma应该被视为机器相关的。不幸地是，ANSI pragmas的语法使得我们无法将其隔离到机器相关的头文件中了。</p>
<p>Pragmas分为两类。优化相关的可以被安全地忽略。而那些影响系统行为(需要pragmas)的Pragmas则不能忽略。需要的pragmas应该结合#ifdef使用，这样如果一个pragma都没有选到，编译过程将退出。</p>
<p>两个编译器可能通过两个不同的方式使用同一个给定的pragma。例如，一个编译器可能使用haggis发出一个优化信号。而另一个可能使用它暗示一个特 定语句，一旦执行到此，程序应该退出。不过，一旦使用了pragma，它们必须总是被机器相关的#ifdef包围。对于非ANSI编译器，Pragmas 必须总是被#ifdef。确保对#pragma的#进行缩进，否则一些较老的预处理器处理它时会挂起。</p>
<p><font face="Courier New">#if defined(__STDC__) &amp;&amp; defined(USE_HAGGIS_PRAGMA)<br />
	&nbsp;&nbsp;&nbsp; #pragma (HAGGIS)<br />
	#endif</font></p>
<p><i>&nbsp;&nbsp;&nbsp; &quot;ANSI标准中描述的&#39;#pragma&#39;命令具有任意实现定义的影响。在GNU C预处理中，&#39;#pragma&#39;首先尝试运行游戏&#39;rogue&#39;；如果失败，它将尝试运行游戏&#39;hack&#39;；如果失败，它将尝试运行GNU Emacs显示汉诺塔；如果失败，它将报告一个致命错误。无论如何，预处理将不再继续。&quot;</i></p>
<p><i>&nbsp;&nbsp;&nbsp; &#8212; GNU CC 1.34 C预处理手册。</i></p>
<p><b><big>18. 特殊考虑</big></b></p>
<p>这节包含一些杂项：&lsquo;做&#39;与&#39;不做&#39;。</p>
<p>&nbsp;&nbsp;&nbsp; * 不要通过宏替换来改变语法。这将导致程序对于所有人都是难以理解的，除了那个肇事者。</p>
<p>&nbsp;&nbsp;&nbsp; * 不要在需要离散值的地方使用浮点变量。使用一个浮点数作为循环计数器无疑是搬起石头砸自己的脚。总是用&lt;=或&gt;=测试浮点数，对它们永远不要 用精确比较(==或!=)。</p>
<p>&nbsp;&nbsp;&nbsp; * 编译器也有bug。常见且高发的问题包括结构体赋值和位字段。你无法泛泛的预测一个编译器都有哪些bug。但你可以在程序中避免使用那些已知的在所有编译 器上都存在问题的结构。你无法让你写的任何代码都是有用的，你可能仍然会遇到bug，并且在这期间编译器很可能会被修复。因此，只有当你被强制使 用某个特定的充斥bug的编译器时，你才应该&quot;围绕&quot;着编译器bug写代码。</p>
<p>&nbsp;&nbsp;&nbsp; * 不要依赖自动代码美化工具。良好代码风格的主要受益者就是代码的编写者，并且尤其在手写算法或伪代码的早期设计阶段。自动代码美化工具只应该用在那些已经 完成、语法正确并且此后不能满足当空白和缩进被更为关注的要求时。伴随着对细致程序员的细节的关注，对于那些将函数或文件布局解释清楚的工作，程 序员们会做得更好(换句话说，一些视觉布局是由意图而不是语法决定的，美化工具无法了解到程序员的思想)。粗心的程序员应该学习成为一个细致的程 序员，而不是依赖美化工具让代码可读性更好。</p>
<p>&nbsp;&nbsp;&nbsp; * 意外地遗漏逻辑比较表达式中的第二个=是一个常犯的问题。使用显式测试。避免对赋值使用隐式测试。</p>
<p><font face="Courier New">abool = bbool;<br />
	if (abool) { &#8230;</font></p>
<p>当嵌入的赋值表达式使用时，确保测试是显式的，这样后续它就无法被&quot;修复&quot;了。</p>
<p><font face="Courier New">while ((abool = bbool) != FALSE) { &#8230;<br />
	while (abool = bbool) { &#8230;&nbsp;&nbsp;&nbsp; /* VALUSED */<br />
	while (abool = bbool, abool) { &#8230;</font></p>
<p>&nbsp;&nbsp;&nbsp; 显式地注释那些在正常控制流之外被修改的变量，或其他可能在维护过程中中断的代码。</p>
<p>&nbsp;&nbsp;&nbsp; 现代编译器会自动将变量放到寄存器中。对于你认为最关键的变量慎用寄存器。在极端情况下，用寄存器标记2-4个最为关键的值，并且将剩余的标记为 REGISTER。后者在那些具有较多寄存器的机器上可以#define为寄存器。</p>
<p><big><b>19. Lint</b></big></p>
<p>Lint是一个C程序检查工具，用于检查C语言源码文件，探测和报告诸如类型不兼容、函数定义与调用不一致以及潜在的bug等情况。强烈建议在所 有程序上使用lint工具，并且期望大多数工程将lint作为官方验收程序的一部分。</p>
<p>应该注意的是使用lint的最好方法不是将lint作为官方验收之前的一道必须跨过的栅栏，而是作为一个在代码发生添加或变更之后使用的工具。 Lint可以发现一些隐藏的bug并且可以在问题发生前保证程序的可移植性。lint产生的许多信息确实暗示了一些事情是错误的。一个有意思的故 事是关于一个漏掉了fprintf的一个参数的程序：</p>
<p><font face="Courier New">fprintf (&quot;Usage: foo -bar &lt;file&gt;\n&quot;);</font></p>
<p>作者从未有过一个问题。但每当一个正常用户在命令行上犯错，这个程序就会产生一个core。许多版本的lint工具都能发现这个问题。</p>
<p>大多lint选项都值得我们学习。一些选项可能在合法的代码上给出警告，但它们也会捕捉到许多把事情搞遭的代码。注意&#39;&#8211;p&#39;只能为库的一个子 集检查函数调用和类型的一致性，因此程序为了最大化的覆盖检查，应该同时进行带&#8211;p和不带&#8211;p的lint检查。</p>
<p>Lint也可以识别代码里的一些特殊注释。这些注释可以强制让lint在发现问题时关闭警告输出，还可以作为一些特殊代码的文档。</p>
<p><big><b>20. Make</b></big></p>
<p>另外一个非常有用的工具是make。在开发过程中，make只会重新编译那些上次make后发生了改变的模块。它也可以用于自动化其他任务。一些 常见的约定包括：</p>
<p>all<br />
	执行所有二进制文件的构建过程</p>
<p>clean<br />
	删除所有中间文件</p>
<p>debug<br />
	构建一个测试用二进制文件a.out或debug</p>
<p>depend<br />
	制作可传递的依赖关系</p>
<p>install<br />
	安装二进制文件，库等</p>
<p>deinstall<br />
	取消安装</p>
<p>mkcat<br />
	安装手册</p>
<p>lint<br />
	运行lint工具</p>
<p>print/list<br />
	制作一个所有源文件的拷贝</p>
<p>shar<br />
	为所有源文件制作一个shar文件</p>
<p>spotless<br />
	执行make clean，并将源码存入版本控制工具。注意：不会删除Makefile，即便它是一个源文件。</p>
<p>source<br />
	撤销spotless所做的事情。</p>
<p>tags<br />
	运行ctags(建议使用-t标志)</p>
<p>rdist<br />
	分发源码到其他主机</p>
<p>file.c<br />
	从版本控制系统中检出这个文件</p>
<p>除此之外，通过命令行也可以定义Makefile使用的值(如&quot;CFLAGS&quot;)或源码中使用的值(如&quot;DEBUG&quot;)。</p>
<p><b><big>21. 工程相关的标准 </big></b></p>
<p>除了这里提到内容外，每个独立的工程都期望能建立附加标准。下面是每个工程程序管理组需要考虑的问题中的一部分：</p>
<p>&nbsp;&nbsp;&nbsp; * 哪些额外的命名约定需要遵守？尤其是，那些用于全局数据的功能归类以及结构体或联合体成员名字的系统化的前缀约定非常有用。</p>
<p>&nbsp;&nbsp;&nbsp; * 什么样的头文件组织适合于工程特定的数据体系结构？</p>
<p>&nbsp;&nbsp;&nbsp; * 应该建立什么样的规程来审核lint警告？需要确立一个与lint选项一致的宽容度，保证lint不会针对一些不重要的问题给出警告，但同时保证真正的bug或不一致问题不被隐藏。</p>
<p>&nbsp;&nbsp;&nbsp; * 如果一个工程建立了自己的档案库，它应该计划向系统管理员提供一个lint库文件。这个lint库文件允许lint工具检查对库函数的兼容性使用。</p>
<p>&nbsp;&nbsp;&nbsp; * 需要使用哪种版本控制工具？</p>
<p><big><b>22. 结论</b></big></p>
<p>这里描述了一套C语言编程风格的标准。其中最重要的几点是：</p>
<p>&nbsp;&nbsp;&nbsp; * 合理使用空白和注释，使得我们通过代码布局就可以清楚地看出程序的结构。使用简单表达式、语句和函数，使他们可以很容易地被理解。</p>
<p>&nbsp;&nbsp;&nbsp; * 记住，在将来某个时候你或其他人很可能会被要求修改代码或让代码运行在一台不同的机器上。精心编写代码，使得其可以移植到尚不确定的机器上。局部化你的优化，因为这些优化经常让人困惑，并且对于该优化措施是否适合其他机器我们持悲观态度。</p>
<p>&nbsp;&nbsp;&nbsp; * 许多风格选择是主观武断的。保持代码风格一致比遵循这些绝对的风格规则更重要(尤其是与组织内部标准保持一致)。混用风格比任何一种糟糕的风格都更加糟糕。</p>
<p>无论采用哪种标准，如果认为该标准有用就必须遵循它。如果你觉得遵循某条标准时有困难，不要仅仅忽略它们，而是在和你当地的大师或组织内的有经验的程序员讨论后再做决定。</p>
<p><big><b>23. 参考资料</b></big></p>
<p>&nbsp;&nbsp;&nbsp; 1. B.A. Tague, C Language Portability, Sept 22, 1977. This document issued by department 8234 contains three memos by R.C. Haight, A.L. Glasser, and T.L. Lyon dealing with style and portability.<br />
	&nbsp;&nbsp;&nbsp;<br />
	&nbsp;&nbsp;&nbsp; 2. S.C. Johnson, Lint, a C Program Checker, Unix Supplementary Documents, November 1986.<br />
	&nbsp;&nbsp;&nbsp;<br />
	&nbsp;&nbsp;&nbsp; 3. R.W. Mitze, The 3B/PDP-11 Swabbing Problem, Memorandum for File, 1273-770907.01MF, September 14, 1977.<br />
	&nbsp;&nbsp;&nbsp;<br />
	&nbsp;&nbsp;&nbsp; 4. R.A. Elliott and D.C. Pfeffer, 3B Processor Common Diagnostic Standards- Version 1, Memorandum for File, 5514-780330.01MF, March 30, 1978.</p>
<p>&nbsp;&nbsp;&nbsp; 5. R.W. Mitze, An Overview of C Compilation of Unix User Processes on the 3B, Memorandum for File, 5521-780329.02MF, March 29, 1978.</p>
<p>&nbsp;&nbsp;&nbsp; 6. B.W. Kernighan and D.M. Ritchie, The C Programming Language, Prentice Hall 1978, Second Ed. 1988, ISBN 0-13-110362-8.</p>
<p>&nbsp;&nbsp;&nbsp; 7. S.I. Feldman, Make &#8212; A Program for Maintaining Computer Programs, UNIXSupplementary Documents, November 1986.</p>
<p>&nbsp;&nbsp;&nbsp; 8. Ian Darwin and Geoff Collyer, Can&#39;t Happen or / NOTREACHED / or Real Programs Dump Core, USENIX Association Winter Conference, Dallas 1985<br />
	Proceedings.</p>
<p>&nbsp;&nbsp;&nbsp; 9. Brian W. Kernighan and P. J. Plauger The Elements of Programming Style. McGraw-Hill, 1974, Second Ed. 1978, ISBN 0-07-034-207-5.</p>
<p>&nbsp;&nbsp;&nbsp; 10. J. E. Lapin Portable C and UNIX System Programming, Prentice Hall 1987, ISBN 0-13-686494-5.</p>
<p>&nbsp;&nbsp;&nbsp; 11. Ian F. Darwin, Checking C Programs with lint, O&#39;Reilly &amp; Associates, 1989. ISBN 0-937175-30-7.</p>
<p>&nbsp;&nbsp;&nbsp; 12. Andrew R. Koenig, C Traps and Pitfalls, Addison-Wesley, 1989. ISBN 0-201-17928-8.<br />
	&nbsp;</p>
<p style='text-align:left'>&copy; 2013, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2013/11/26/the-full-text-of-recommended-c-style-and-coding-standards/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>buildc 0.3.1版本发布</title>
		<link>https://tonybai.com/2013/07/15/buildc-0-3-1-release/</link>
		<comments>https://tonybai.com/2013/07/15/buildc-0-3-1-release/#comments</comments>
		<pubDate>Mon, 15 Jul 2013 08:54:52 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[Buildc]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[GNU]]></category>
		<category><![CDATA[Make]]></category>
		<category><![CDATA[Makefile]]></category>
		<category><![CDATA[Opensource]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[博客]]></category>
		<category><![CDATA[命令行变量]]></category>
		<category><![CDATA[工作]]></category>
		<category><![CDATA[开源]]></category>
		<category><![CDATA[构建]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=1299</guid>
		<description><![CDATA[随着buildc在内部应用的深入，buildc逐渐进入了以内部需求和问题为主要驱动力的演化模式。我们内部的C应用多是后端服务类应用，个人 觉得具有一定代表性。buildc最初就是为了针对这类C应用而设计的。因此我们内部的需求和问题应该也同样具有一定代表性，而这种演化模式在一 段时间范围内还是有意义的。 buildc 0.3.1版本修正了上一版本的若干bug，并增加了两个新功能。 * 提高容错能力 buildc对第三方库的组织结构有着严格的要求，一般是： package_name/ &#160;&#160;&#160;&#160;&#160; version/ &#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; CPU_MODE_OS/ &#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; include/ &#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; lib/ 一般来说，第三方库会由组织内特定人员进行管理和维护，第三方库服务器上的目录结构不会出现组织错误的情况。但buildc 0.3.0还是遇到特例了：当某个package的第三级目录为空时，buildc 0.3.0版本会抛出异常。为此，buildc 0.3.1增加了对这块逻辑的容错处理： &#160;&#160;&#160;&#160; 1. 如果目录是空目录，直接略过。 &#160;&#160;&#160;&#160; 2. 如果目录存在合法的目录，cpu_mode_os，加入.buildc.repository中 &#160;&#160;&#160;&#160; 3. 如果目录中存在合法的目录和不合法的目录，略过不合法的目录。 * 支持命令行变量 有些项目针对不同客户有不同的功能版本，但代码是一份，针对不同客户的Release版本用一些特定的宏开关控制，而这些功能开关需要在编译构建 期指定。比如最初版本的buildc.cfg中的片段如下： custom_defs = ['-std=gnu99', '-DLOGLEVEL=1', '-DUSE_NM'] 后A省的客户希望LOGLEVEL用2级，不需要NM(网管)功能，而B省的客户希望LOGLEVEL用2级同时也使用NM功能，那我们的 custom_defs就需要有多种配置了，例如： if province == &#34;A&#34;: &#160;&#160;&#160; custom_defs = ['-std=gnu99', '-DLOGLEVEL=2'] elif provice == &#34;B&#34; &#160;&#160;&#160; [...]]]></description>
			<content:encoded><![CDATA[<p>随着<a href="http://code.google.com/p/buildc">buildc</a>在内部应用的深入，buildc逐渐进入了以内部需求和问题为主要驱动力的演化模式。我们内部的C应用多是后端服务类应用，个人 觉得具有一定代表性。<a href="http://tonybai.com/2011/12/08/buildc-a-building-assistant-tool-for-c-app/">buildc</a>最初就是为了针对这类C应用而设计的。因此我们内部的需求和问题应该也同样具有一定代表性，而这种演化模式在一 段时间范围内还是有意义的。</p>
<p><a href="https://buildc.googlecode.com/files/buildc-0.3.1.tar.gz">buildc 0.3.1版本</a>修正了上一版本的若干bug，并增加了两个新功能。</p>
<p><b>* 提高容错能力</b></p>
<p>buildc对第三方库的组织结构有着严格的要求，一般是：</p>
<p><font face="Courier New">package_name/<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; version/<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; CPU_MODE_OS/<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; include/<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; lib/</font></p>
<p>一般来说，第三方库会由组织内特定人员进行管理和维护，第三方库服务器上的目录结构不会出现组织错误的情况。但<a href="http://tonybai.com/2013/05/11/buildc-0-3-0-release/">buildc 0.3.0</a>还是遇到特例了：当某个package的第三级目录为空时，buildc 0.3.0版本会抛出异常。为此，buildc 0.3.1增加了对这块逻辑的容错处理：<br />
	&nbsp;&nbsp;&nbsp;&nbsp; 1. 如果目录是空目录，直接略过。<br />
	&nbsp;&nbsp;&nbsp;&nbsp; 2. 如果目录存在合法的目录，cpu_mode_os，加入.buildc.repository中<br />
	&nbsp;&nbsp;&nbsp;&nbsp; 3. 如果目录中存在合法的目录和不合法的目录，略过不合法的目录。</p>
<p><b>* 支持命令行变量</b></p>
<p>有些项目针对不同客户有不同的功能版本，但代码是一份，针对不同客户的Release版本用一些特定的宏开关控制，而这些功能开关需要在编译构建 期指定。比如最初版本的buildc.cfg中的片段如下：</p>
<p><font face="Courier New">custom_defs = ['-std=gnu99', '-DLOGLEVEL=1', '-DUSE_NM']</font></p>
<p>后A省的客户希望LOGLEVEL用2级，不需要NM(网管)功能，而B省的客户希望LOGLEVEL用2级同时也使用NM功能，那我们的 custom_defs就需要有多种配置了，例如：</p>
<p><font face="Courier New">if province == &quot;A&quot;:<br />
	&nbsp;&nbsp;&nbsp; custom_defs = ['-std=gnu99', '-DLOGLEVEL=2']<br />
	elif provice == &quot;B&quot;<br />
	&nbsp;&nbsp;&nbsp; custom_defs = ['-std=gnu99', '-DLOGLEVEL=2', '-DUSE_NM']<br />
	else:<br />
	&nbsp;&nbsp;&nbsp; custom_defs = ['-std=gnu99', '-DLOGLEVEL=1', '-DUSE_NM']</font></p>
<p>province这个变量可以定义在buildc.cfg中，但每次针对不同省份Release时，需要手工修改province变量的值，这样 十分麻烦。因此我们想到是否可以让buildc像Make那样支持<a href="http://tonybai.com/2011/05/19/use-command-line-vars-of-make/">命令行变量</a>呢，就像这样：</p>
<p><font face="Courier New">buildc config make province=&quot;B&quot;</font></p>
<p>于是乎buildc 0.3.1版本就实现了这个功能，你可以在buildc config make或buildc pack中使用buildc的命令行变量，命令行变量支持var=value形式，其中value支持如下几种值：</p>
<p><font face="Courier New">var=1<br />
	var=on<br />
	var=&quot;on&quot;</font></p>
<p>对于var=on，在buildc内部会将var=on转化为var=&quot;on&quot;，否则python会提示找不到on的定义。</p>
<p><b>* 支持指定项目配置文件（buildc.cfg)</b></p>
<p>当功能开关变得很多时，我们往往很难记住那么多<a href="http://tonybai.com/2013/07/09/an-implementation-of-python-commandline-variables/">命令行变量</a>，我们可能就会为每个项目保存多个项目构建的配置文件，比如 buildc_for_liaoning.cfg、buildc_for_beijing.cfg等。而以前的buildc只默认支持 buildc.cfg这样一种配置文件，无法支持这类需求，因此buildc 0.3.1增加了指定项目配置文件功能。</p>
<p>如果你要为A省客户发布，你可以敲入buildc config make &#8211;config=PATH_OF_CONFIG_FILE，buildc就会加载你指定的配置文件了，而不是默认的buildc.cfg。</p>
<p style='text-align:left'>&copy; 2013, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2013/07/15/buildc-0-3-1-release/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>跨过BUG查找的”最后一公里”</title>
		<link>https://tonybai.com/2013/06/18/walk-through-the-last-mile-of-bugfix/</link>
		<comments>https://tonybai.com/2013/06/18/walk-through-the-last-mile-of-bugfix/#comments</comments>
		<pubDate>Tue, 18 Jun 2013 10:43:34 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[Bug]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[Debug]]></category>
		<category><![CDATA[GCC]]></category>
		<category><![CDATA[GDB]]></category>
		<category><![CDATA[GNU]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[博客]]></category>
		<category><![CDATA[学习]]></category>
		<category><![CDATA[工作]]></category>
		<category><![CDATA[思考]]></category>
		<category><![CDATA[感悟]]></category>
		<category><![CDATA[最后一公里]]></category>
		<category><![CDATA[程序员]]></category>
		<category><![CDATA[缓冲区溢出]]></category>
		<category><![CDATA[编译器]]></category>
		<category><![CDATA[调试]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=1288</guid>
		<description><![CDATA[如果你看到一个C程序员在通宵熬夜神情紧张地对着电脑敲代码或阅读代码，多数只有两种可能：一是为了赶进度；二就是查找内存Bug。 &#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160;&#160; &#160; &#8212; 个人感悟 &#160; 昨晚搞到凌晨一点多，终于算是把一个棘手的Bug的来龙去脉搞清楚了。截至到今天，这个Bug已经困扰了项目组两个核心开发同事达三周之久了。 这个Bug的确很难查找： &#160;&#160; &#8211; 首先模拟环境下无法复现该Bug； &#160;&#160; &#8211; 生产环境下该Bug是随机出现的，发生频率十分低； &#160;&#160; &#8211; Bug出现时并未有dump core等明显异常现象出现，系统依旧运行良好。 得到Bug报告后，我的两位同事就开始对bug引发的问题现象进行了分析，得出了内存被污染的初步结论。之后又在生产环境做了GDB attach到进程的调试，甚至替换了生产环境的版本，利用传统的print语句在关键路径上输出提示信息，试图找到引发Bug的真正原因。但做过这些 后，所能得到的结论依旧停留在内存被污染，至于怎么被污染的、在哪个业务流程上被污染的却无从得知。无奈之下，两位同事开始根据 subversion的commit history进行代码比对和分析，试图查找到哪些新增或修改的代码引发了Bug。代码修改量小还好，如果修改数量巨大，这种代码比对就好比大海捞针，我 们无法保证注意力自始自终是集中的，结果两位同事也的确没有从代码变更中发现什么蛛丝马迹。这类Bug会让你有一种有力无处施展的感觉，面对这样 的Bug，我的两位开发人员似乎也失去了信心和思路。 下面简要描述一下这个Bug： 有这样一个字段数目众多的结构体foo_t，这里仅列出bug相关的几个字段e、c、flag、pdata： struct foo_t { &#160;&#160;&#160; &#8230; &#8230; &#160;&#160;&#160; char e[XX_SIZE]; &#160;&#160;&#160; char c[XX_SIZE]; &#160;&#160;&#160; char flag; &#160;&#160;&#160; data_t *pdata; &#160;&#160;&#160; &#8230; &#8230; [...]]]></description>
			<content:encoded><![CDATA[<p><i>如果你看到一个C程序员在通宵熬夜神情紧张地对着电脑敲代码或阅读代码，多数只有两种可能：一是为了赶进度；二就是查找内存Bug。</i><br />
	<i>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp; &nbsp; &#8212; 个人感悟</i><br />
	&nbsp;<br />
	昨晚搞到凌晨一点多，终于算是把一个棘手的Bug的来龙去脉搞清楚了。截至到今天，这个Bug已经困扰了项目组两个核心开发同事达三周之久了。</p>
<p>这个Bug的确很难查找：</p>
<p>&nbsp;&nbsp; &#8211; 首先模拟环境下无法复现该Bug；<br />
	&nbsp;&nbsp; &#8211; 生产环境下该Bug是随机出现的，发生频率十分低；<br />
	&nbsp;&nbsp; &#8211; Bug出现时并未有dump core等明显异常现象出现，系统依旧运行良好。</p>
<p>得到Bug报告后，我的两位同事就开始对bug引发的问题现象进行了分析，得出了内存被污染的初步结论。之后又在生产环境做了<a href="http://tonybai.com/2006/01/08/debug-multiple-process-program-using-gdb/">GDB</a> attach到进程的调试，甚至替换了生产环境的版本，利用传统的print语句在关键路径上输出提示信息，试图找到引发Bug的真正原因。但做过这些 后，所能得到的结论依旧停留在内存被污染，至于怎么被污染的、在哪个业务流程上被污染的却无从得知。无奈之下，两位同事开始根据 subversion的commit history进行代码比对和分析，试图查找到哪些新增或修改的代码引发了Bug。代码修改量小还好，如果修改数量巨大，这种代码比对就好比大海捞针，我 们无法保证注意力自始自终是集中的，结果两位同事也的确没有从代码变更中发现什么蛛丝马迹。这类Bug会让你有一种有力无处施展的感觉，面对这样 的Bug，我的两位开发人员似乎也失去了信心和思路。</p>
<p>下面简要描述一下这个Bug：</p>
<p>有这样一个字段数目众多的结构体foo_t，这里仅列出bug相关的几个字段e、c、flag、pdata：</p>
<p><font face="Courier New">struct foo_t {<br />
	&nbsp;&nbsp;&nbsp; &#8230; &#8230;<br />
	&nbsp;&nbsp;&nbsp; char e[XX_SIZE];<br />
	&nbsp;&nbsp;&nbsp; char c[XX_SIZE];<br />
	&nbsp;&nbsp;&nbsp; char flag;<br />
	&nbsp;&nbsp;&nbsp; data_t *pdata;<br />
	&nbsp;&nbsp;&nbsp; &#8230; &#8230;<br />
	}；</font></p>
<p>业务逻辑是：</p>
<p><font face="Courier New">if (flag) {<br />
	&nbsp;&nbsp;&nbsp; 处理e、c两个字段；<br />
	}</font><br />
	&nbsp;&nbsp;&nbsp;<br />
	bug现象：值本是1的flag字段被污染，值变成了0，导致e、c两个字段没有被做处理，从而引发业务异常，导致客户投诉。我的同事曾经做过如 下尝试，以确定内存污染的行为特点，她在flag之前又加了一个字段flag1：</p>
<p><font face="Courier New">struct foo_t {<br />
	&nbsp;&nbsp;&nbsp; &#8230; &#8230;<br />
	&nbsp;&nbsp;&nbsp; char e[XX_SIZE];<br />
	&nbsp;&nbsp;&nbsp; char c[XX_SIZE];<br />
	&nbsp;&nbsp;&nbsp; unsigned int flag1;<br />
	&nbsp;&nbsp;&nbsp; char flag;<br />
	&nbsp;&nbsp;&nbsp; data_t *pdata;<br />
	&nbsp;&nbsp;&nbsp; &#8230; &#8230;<br />
	}；</font></p>
<p>在生产环境下运行得到的结果是flag1和flag值正常，但字段c的尾部字节遭到了污染。现象已经十分明确，离真相就差那最后一公里了。</p>
<p>对于上面的内存污染问题，我首先会怀疑在处理flag或c之前的字段时出现了缓冲区溢出，导致后面字段的内容被整体或局部覆盖。不过从bug现象 来看，这个思路也有说不通的地方，那就是为何是c的尾部字段被污染，而不是从头部开始呢？不过我们依旧沿着这个思路追查了e以及e的诸多前驱字 段，细致的分析了代码，但没有发现<a href="http://tonybai.com/2006/09/06/be-careful-of-the-trap-of-overflow/">溢出</a>点。</p>
<p>c或flag的后继字段比如pdata要想污染c或flag则必须具备更多条件，至少要有操作&amp;pdata的代码，之前基本认为这不太可 能。但现在仅有这一条路可以继续走下去了，也只能沿着这条路走下去。事实证明我们走的没错。在后续的处理流程中有这样的一个函数：</p>
<p><font face="Courier New">int func(void *p, int size)</font></p>
<p>这个函数本来是用于处理data_t*变量的，但由于编码者的疏忽，将&amp;pdata传给了p，另外size这个参数也传了一个错误的值， 估计是滥用了copy&amp;paste。而func函数体中对p指向的内存地址做了修改，这个修改直接污染了 ((char*)&amp;pdata + size)起始的那片内存块儿，这就是问题的真正原因所在。这样看来pdata并未污染其所在的foo_t实例中的flag或c字段，而是污染了其他foo_t实例中的flag或c字段，因为这些实例都放在一个mem block pool中的，所以这还是一个随机的远距离内存污染^_^。</p>
<p>我走完了BUG查找的最后那一公里，到达了终点。这个BUG的查找确实不易，但并非遥不可及，为何我的两位同事就停在离真相只有一公里的地方而踌 躇不前了呢？对此我也做了一些考量，希望能在日后的BUG查找方面给予帮助。</p>
<p>要跨过BUG查找的那最后一公里，可从如下几个方面着手努力：</p>
<p><b>* 收罗证据，不放过一处可疑之处</b></p>
<p>这是准备工作，就好比警察查看罪案现场，哪怕是一根毛发，一处异物也不能放过。一般来说我们至少要收集到Bug发生时的各方面信息，包括：</p>
<p>&nbsp;- 系统日志<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; 这个时间点上各个模块的日志都要搜罗到；</p>
<p>&nbsp;- core文件<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 如果bug引发core dump，那core文件是bug查找的最佳入口；</p>
<p>&nbsp;- 通信数据包内容<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 对于很多后端服务程序而言，不合法的通信数据包常常会引发Bug，我经手的类似Bug就不止一起了。必要时通过抓包工具将通信包抓到文件中以备后用。</p>
<p>&nbsp;- CPU/内存/磁盘实时状况<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 千万不要小视这些信息。如果发现CPU过高，则很可能代码存在死循环的可能（后pstack进程号，则可直接找到问题所在）；如果磁盘满，则可以很好解释 数据不完整的异常；如果mem占用过高，则可以解释分配内存异常或性能下降等问题。</p>
<p>&nbsp;- 系统操作日志<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 如果有管理员的操作行为的话，我们也不要放过，将操作日志（一般系统都有保存，并需要对这些日志进行定期审核）截取并保留，以备后用。</p>
<p>&nbsp;- 操作系统/硬件相关异常信息等。<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 如果是因为OS或硬件异常导致的Bug，那搜集到这些信息就太重要了，否则你将付出惨重的Bug查找代价。</p>
<p>Bug查多了你就会有这种感悟：证据用时方恨少啊！</p>
<p><b>* 沉下心</b><b>，保持清晰思路</b></p>
<p>BUG有难有易，简单的Bug大家都能应付，而困难的Bug，就要比拼能力和经验了。要想解决掉Bug，务必要沉下心，不急不躁，这是保持大脑始 终有清晰思路的前提。</p>
<p>能用工具（比如GDB）调试出来的Bug，都不是最难的问题，因为现场就摆在你的面前，你可以看到一切蛛丝马迹。最难的问题最终都是要通过脑力分 析出来的。</p>
<p>解决问题前，要根据之前搜罗的证据，形成自己的查找思路。没有思路是可怕的。没有思路的时候，也不要急于开始查，那样只会乱套。应根据已有的蛛丝 马迹，行成一些思路，哪怕这个思路你自己都不是很肯定，先按这个思路做做看，也许走出一步后，你又能收获新的信息，形成新的思路。就这样敏捷地向 前进，边向前探索边定期回顾。</p>
<p><b>* 知晓原理，缩小查找范围，形成正确思路</b></p>
<p>要保持清晰正确的思路，开发人员对系统的运行原理要做到十分清楚，这样可以缩小查找范围，重点突破。就好比上面的那个bug例子，我们要知道 c/flag被污染有几种潜在的可能，并形成多种思路，然后沿着这几种可能的思路继续走下去。在这次查找过程中，想必两位同事恰恰是在原理这方面 没有理解透彻吧。</p>
<p><b>* 质疑，从自己的代码开始</b></p>
<p>查Bug就要抛弃&ldquo;不可能&rdquo;，拥抱&ldquo;质疑一切&rdquo;。而质疑要从自己的代码开始。程序员或多或少都有一种&ldquo;自负&rdquo;的心态，骨子里会认为自己的代码肯定 是正确的。如果出现问题，一定是其他人代码的问题，哪怕是OS这样总体来说十分稳定的平台也会成为被首先质疑的对象。不过事实证明，错误多出在我 们自己的代码中，毫无理由的去怀疑操作系统、怀疑你使用的第三方库，多半会南辕北辙，浪费你宝贵的查找时间。</p>
<p><b>* 拥抱调试技巧</b><b>和工具</b></p>
<p>必要的调试技巧是Bug查找的基本功底，这些技巧在涉及内存问题查找过程中相当有用。</p>
<p>&nbsp; &#8212; print语句<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; 不用多说，print语句是最简单、最常用的调试手段，在代码任意位置，根据你的需要，输出信息，帮助你分析bug原因。其唯一的缺点就是可能需要你重新 build代码和部署你的应用。</p>
<p>&nbsp; &#8212; gdb切入进程地址空间查看堆栈<br />
	&nbsp; &nbsp; &nbsp; &nbsp;&nbsp; 利用gdb一类的专用调试工具可在代码运行时切入进程地址空间，实时查看数据变化。你也可以在gdb下执行应用，获得同样的效果（适合单进程应用）。<br />
	&nbsp;<br />
	&nbsp; &#8212; 调试版中采用magic number + assert<br />
	&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp; C程序的bug多为内存问题。常见的内存越界访问或污染的调试手段是在代码中为内存块添加magic number，并在特定环节用assert保证该magic number的值是没有被修改的。一旦值改变了，则说明问题发生在执行流的两次assert之间的某个地方，后续可进一步缩小assert间隙，直到定位 到问题。</p>
<p>&nbsp; &#8212; 让bug尽可能的容易复现<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 一个可以在模拟环境下复现的Bug总是比较好查的。出于这个考虑，我们可通过放大问题区域来尽可能更容易的复现bug，比如将一个字节的字段改为4个字 节，这样可能占据更多被污染的区域，比较利于Bug的复现（但这不总是ok的）。</p>
<p><b>* 把握节奏，避免陷入惯性思维</b></p>
<p>一些比较难fix的Bug，其查找过程可能会十分漫长，就像这次我们遇到的这个问题。这就需要我们的开发人员把握好Bug查找的节奏，因为长时间 调试和查问题容易让人陷入惯性思维，反倒不利于Bug的查找。一旦意识到自己进入惯性思维后，可考虑换种活动做做，比如出去散散步、洗个热水澡 等。或者给其他人员讲解你的查找思路，这个过程中自己可能会发现思路上的缺陷，或者由他人指出你思路方面的问题。</p>
<p><b>感觉Bug查找是一门手艺活，要学会慢工出细活，这总比不出活儿的要好，尤其是在面对那些十分诡异的内存Bug时。</b></p>
<p style='text-align:left'>&copy; 2013, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2013/06/18/walk-through-the-last-mile-of-bugfix/feed/</wfw:commentRss>
		<slash:comments>4</slash:comments>
		</item>
		<item>
		<title>再谈C语言位域</title>
		<link>https://tonybai.com/2013/05/21/talk-about-bitfield-in-c-again/</link>
		<comments>https://tonybai.com/2013/05/21/talk-about-bitfield-in-c-again/#comments</comments>
		<pubDate>Tue, 21 May 2013 14:26:14 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[bitfield]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[byteorder]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[endianess]]></category>
		<category><![CDATA[GCC]]></category>
		<category><![CDATA[GNU]]></category>
		<category><![CDATA[Kernel]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Opensource]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[位域]]></category>
		<category><![CDATA[内存布局]]></category>
		<category><![CDATA[博客]]></category>
		<category><![CDATA[可移植]]></category>
		<category><![CDATA[大端]]></category>
		<category><![CDATA[存储单元]]></category>
		<category><![CDATA[学习]]></category>
		<category><![CDATA[小端]]></category>
		<category><![CDATA[工作]]></category>
		<category><![CDATA[开源]]></category>
		<category><![CDATA[移位]]></category>
		<category><![CDATA[程序员]]></category>
		<category><![CDATA[编译器]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=1277</guid>
		<description><![CDATA[我在日常工作中使用C语言中的位域(bit field)的场景甚少，原因大致有二： * 一直从事于服务器后端应用的开发，现在的服务器的内存容量已经达到了数十G的水平，我们一般不需要为节省几个字节而使用内存布局更加紧凑的位域。 * 结构体中位域的实现是平台相关或Compiler相关的，移植性较差，我们不会贸然地给自己造&#8220;坑&#8221;的。 不过近期Linux技术内核社区（www.linux-kernel.cn) mail list中的一个问题让我觉得自己对bit field的理解还欠火候，于是乎我又花了些时间就着那个问题重新温习一遍bit field。 零、对bit field的通常认知 在C语言中，我们可以得到某个字节的内存地址，我们具备了操作任意内存字节的能力；在那个内存空间稀缺的年代，仅仅控制到字节级别还不足以满足C 程序员的胃口，为此C语言中又出现了bit级别内存的&#8220;有限操作能力&#8221; &#8211; 位域。这里所谓的&#8220;有限&#8221;指的是机器的最小粒度寻址单位是字节，我们无法像获得某个字节地址那样得到某个bit的地址，因此我们仅能通过字节的运算来设置 和获取某些bit的值。在C语言中，尝试获得一个bit field的地址是非法操作： struct flag_t { &#160;&#160;&#160; int a : 1; }; struct flag_t flg; printf(&#34;%p\n&#34;, &#38;flg.a); error: cannot take address of bit-field &#8216;a&#8217; 以下是C语言中bit field的一般形式： struct foo_t { &#160;&#160;&#160; unsigned int b1 : n1, &#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; b2 : n2, [...]]]></description>
			<content:encoded><![CDATA[<p>我在日常工作中使用<a href="http://en.wikipedia.org/wiki/C_(programming_language)‎">C语言</a>中的<a href="http://tonybai.com/2006/06/19/understand-bit-fields/">位域</a>(bit field)的场景甚少，原因大致有二：</p>
<p>* 一直从事于服务器后端应用的开发，现在的服务器的内存容量已经达到了数十G的水平，我们一般不需要为节省几个字节而使用内存布局更加紧凑的位域。<br />
	* 结构体中位域的实现是平台相关或Compiler相关的，移植性较差，我们不会贸然地给自己造&ldquo;坑&rdquo;的。</p>
<p>不过近期Linux技术内核社区（www.linux-kernel.cn) mail list中的一个问题让我觉得自己对<a href="http://en.wikipedia.org/wiki/Bit_field">bit field</a>的理解还欠火候，于是乎我又花了些时间就着那个问题重新温习一遍bit field。</p>
<p><b>零、对bit field的</b><b>通常认知</b></p>
<p>在C语言中，我们可以得到某个字节的内存地址，我们具备了操作任意内存字节的能力；在那个内存空间稀缺的年代，仅仅控制到字节级别还不足以满足C 程序员的胃口，为此C语言中又出现了bit级别内存的&ldquo;有限操作能力&rdquo; &#8211; 位域。这里所谓的&ldquo;有限&rdquo;指的是机器的最小粒度寻址单位是字节，我们无法像获得某个字节地址那样得到某个bit的地址，因此我们仅能通过字节的运算来设置 和获取某些bit的值。<b>在C语言中，尝试获得一个bit field的地址是非法操作</b>：</p>
<p><font face="Courier New">struct flag_t {<br />
	&nbsp;&nbsp;&nbsp; int a : 1;<br />
	};</font></p>
<p><font face="Courier New">struct flag_t flg;<br />
	printf(&quot;%p\n&quot;, &amp;flg.a);</font></p>
<p><font face="Courier New">error: cannot take address of bit-field &lsquo;a&rsquo;</font></p>
<p>以下是C语言中bit field的一般形式：</p>
<p><font face="Courier New">struct foo_t {<br />
	&nbsp;&nbsp;&nbsp; unsigned int b1 : n1,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; b2 : n2,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#8230; &#8230;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; bn : nk;<br />
	};</font></p>
<p>其中n1，n2，nk为对应位域所占据的bit数。</p>
<p>位域(bit field)的出现让我们可以<b>用变量名代表某些bit，并通过变量名直接获得和设置一些内存中bit的值</b>，而不是通 过晦涩难以理解的位操作来进行，例如：</p>
<p><font face="Courier New">struct foo_t {<br />
	&nbsp;&nbsp;&nbsp; unsigned int a : 3,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; b : 2,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; c : 4;<br />
	};</font></p>
<p><font face="Courier New">struct foo_t f;<br />
	f.a = 3;<br />
	f.b = 1;<br />
	f.c = 12;</font></p>
<p>另外使用位域我们可以在展现和存储相同信息的同时，自定义<b>更加紧凑的内存布局</b>，节约内存的使用量。这使得bit field在嵌入式领域，在驱动程序领域得到广泛的应用，比如可以仅用两个字节就可以将tcpheader从dataoffset到fin的信息全部表示 和存储起来：</p>
<p><font face="Courier New">struct tcphdr {<br />
	&nbsp;&nbsp;&nbsp; &#8230; &#8230;<br />
	&nbsp;&nbsp;&nbsp; __u16&nbsp;&nbsp; doff:4,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; res1:4,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; cwr:1,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ece:1,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; urg:1,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ack:1,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; psh:1,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; rst:1,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; syn:1,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fin:1;<br />
	&nbsp;&nbsp;&nbsp; &#8230; &#8230;<br />
	};</font></p>
<p><b>一、存储单元(storage unit)</b></p>
<p><a href="http://tonybai.com/2005/07/28/introduction-on-c-standard-overview-series/">C标准</a>允许unsigned int/signed int/int类型的位域声明，<a href="http://tonybai.com/2011/08/31/simplify-coding-in-c99/">C99</a>中加入了_Bool类型的位域。但像<a href="http://tonybai.com/2006/03/14/explain-gcc-warning-options-by-examples/">Gcc</a>这样的编译器自行加入了一些扩展，比如支持short、char等整型类 型的位域字段，使用其他类型声明位域将得到错误的结果，比如：</p>
<p><font face="Courier New">struct flag_t {<br />
	&nbsp;&nbsp;&nbsp; char* a : 1;<br />
	};<br />
	&nbsp;error: bit-field &lsquo;a&rsquo; has invalid type</font></p>
<p>C编译器究竟是如何为bit field分配存储空间的呢？我们以Gcc编译器(<a href="http://tonybai.com/2012/12/04/upgrade-ubuntu-to-1204-lts/">Ubuntu 12.04</a>.2 x86_64 Gcc 4.7.2 )为例一起来探究一下。</p>
<p>我们先来看几个基本的bit field类型的例子：</p>
<p><font face="Courier New">struct bool_flag_t {<br />
	&nbsp;&nbsp;&nbsp; _Bool a : 1,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; b : 1;<br />
	};</font></p>
<p><font face="Courier New">struct char_flag_t {<br />
	&nbsp;&nbsp;&nbsp; unsigned char a : 2,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; b : 3;<br />
	};</font></p>
<p><font face="Courier New">struct short_flag_t {<br />
	&nbsp;&nbsp;&nbsp; unsigned short a : 2,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; b : 3;<br />
	};</font></p>
<p><font face="Courier New">struct int_flag_t {<br />
	&nbsp;&nbsp;&nbsp; int a : 2,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; b : 3;<br />
	};</font></p>
<p><font face="Courier New">int<br />
	main()<br />
	{<br />
	&nbsp;&nbsp;&nbsp; printf(&quot;%ld\n&quot;, sizeof(struct bool_flag_t));<br />
	&nbsp;&nbsp;&nbsp; printf(&quot;%ld\n&quot;, sizeof(struct char_flag_t));<br />
	&nbsp;&nbsp;&nbsp; printf(&quot;%ld\n&quot;, sizeof(struct short_flag_t));<br />
	&nbsp;&nbsp;&nbsp; printf(&quot;%ld\n&quot;, sizeof(struct int_flag_t));</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; return 0;<br />
	}</font></p>
<p>编译执行后的输出结果为：<br />
	<font face="Courier New">1<br />
	1<br />
	2<br />
	4</font></p>
<p>可以看出Gcc为不同类型的bit field分配了不同大小的基本内存空间。_Bool和char类型的基本存储空间为1个字节；short类型的基本存储空间为2个字节，int型的为4 个字节。这些空间的分配是基于结构体内部的bit field的size没有超出基本空间的界限为前提的。以short_flag_t为例：</p>
<p><font face="Courier New">struct short_flag_t {<br />
	&nbsp;&nbsp;&nbsp; unsigned short a : 2,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; b : 3;<br />
	};</font></p>
<p>a、b两个bit field总共才使用了5个bit的空间，所以Compiler只为short_flag_t分配一个基本存储空间就可以存储下这两个bit field。如果bit field的size变大，size总和超出基本存储空间的size时，编译器会如何做呢？我们还是看例子：</p>
<p><font face="Courier New">struct short_flag_t {<br />
	&nbsp;&nbsp;&nbsp; unsigned short a : 7,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; b : 10;<br />
	};</font></p>
<p>将short_flag_t中的两个bit字段的size增大后，我们得到的sizeof(struct short_flag_t)变成了4，显然Compiler发现一个基础存储空间已经无法存储下这两个bit field了，就又为short_flag_t多分配了一个基本存储空间。这里我们所说的基本存储空间就称为<b>&ldquo;存储单元(storage unit)&rdquo;</b><b>。</b>它是Compiler在给bit field分配内存空间时的基本单位，并且这些分配给bit field的内存是以存储单元大小的整数倍递增的。但从上面来看，<b>不同类型bit field的存储单元大小是不同的</b>。</p>
<p>sizeof(struct short_flag_t)变成了4，那a和b有便会有至少两种内存布局方式：<br />
	* a、b紧邻<br />
	* b在下一个可存储下它的存储单元中分配内存</p>
<p>具体采用哪种方式，是Compiler相关的，这会影响到bit field的可移植性。我们来测试一下Gcc到底采用哪种方式：</p>
<p><font face="Courier New">void<br />
	dump_native_bits_storage_layout(unsigned char *p, int bytes_num)<br />
	{</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; union flag_t {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; unsigned char c;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; struct base_flag_t {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; unsigned int p7:1,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; p6:1,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; p5:1,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; p4:1,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; p3:1,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; p2:1,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; p1:1,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; p0:1;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; } base;<br />
	&nbsp;&nbsp;&nbsp; } f;</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; for (int i = 0; i &lt; bytes_num; i++) {<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; f.c = *(p + i);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; printf(&quot;%d%d%d%d %d%d%d%d &quot;,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; f.base.p7,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; f.base.p6,&nbsp;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; f.base.p5,&nbsp;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; f.base.p4,&nbsp;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; f.base.p3,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; f.base.p2,&nbsp;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; f.base.p1,&nbsp;<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; f.base.p0);<br />
	&nbsp;&nbsp;&nbsp; }<br />
	&nbsp;&nbsp;&nbsp; printf(&quot;\n&quot;);<br />
	}</font></p>
<p><font face="Courier New">struct short_flag_t {<br />
	&nbsp;&nbsp;&nbsp; unsigned short a : 7,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; b : 10;<br />
	};</font></p>
<p><font face="Courier New">&nbsp;struct short_flag_t s;<br />
	&nbsp;memset(&amp;s, 0, sizeof(s));<br />
	&nbsp;s.a = 113; /* 0111 0001 */<br />
	&nbsp;s.b = 997; /* 0011 1110 0101 */</font></p>
<p><font face="Courier New">&nbsp;dump_native_bits_storage_layout((unsigned char*)&amp;s, sizeof(s));</font><br />
	&nbsp;<br />
	编译执行后的输出结果为：<font face="Courier New"> 1000 1110 0000 0000 1010 0111 1100 0000</font>。可以看出Gcc采用了第二种方式，即在为a分配内存后，发现该存储单元剩余的空间(9 bits)已经无法存储下字段b了，于是乎Gcc又分配了一个存储单元(2个字节)用来为b分配空间，而a与b之间也因此存在了空隙。</p>
<p>我们还可以通过<b>匿名0长度位域字段</b>的语法强制位域在下一个存储单元开始分配，例如：</p>
<p><font face="Courier New">struct short_flag_t {<br />
	&nbsp;&nbsp;&nbsp; unsigned short a : 2,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; b : 3;<br />
	};</font><br />
	这个结构体本来是完全可以在一个存储单元(2字节)内为a、b两个位域分配空间的。如果我们非要让b放在与a不同的存储单元中，我们可以通过加入 匿名0长度位域的方法来实现：</p>
<p><font face="Courier New">struct short_flag_t {<br />
	&nbsp;&nbsp;&nbsp; unsigned short a : 2;<br />
	&nbsp;&nbsp;&nbsp; unsigned short&nbsp;&nbsp; : 0;<br />
	&nbsp;&nbsp;&nbsp; unsigned short b : 3;<br />
	};</font></p>
<p>这样声明后，sizeof(struct short_flag_t)变成了4。</p>
<p><font face="Courier New">&nbsp;struct short_flag_t s;<br />
	&nbsp;memset(&amp;s, 0, sizeof(s));<br />
	&nbsp;s.a = 2; /* 10 */<br />
	&nbsp;s.b = 4; /* 100 */</font></p>
<p><font face="Courier New">&nbsp;dump_native_bits_storage_layout((unsigned char*)&amp;s, sizeof(s));</font></p>
<p>执行后，输出的结果为：</p>
<p><font face="Courier New">0100 0000 0000 0000 0010 0000 0000 0000</font></p>
<p>可以看到位域b被强制放到了第二个存储单元中。如果没有那个匿名0长度的位域，那结果应该是这样的：</p>
<p><font face="Courier New">0100 1000 0000 0000</font></p>
<p>最后位域的长度是不允许超出其类型的最大长度的，比如：</p>
<p><font face="Courier New">struct short_flag_t {<br />
	&nbsp;&nbsp;&nbsp; short a : 17;<br />
	};</font></p>
<p><font face="Courier New">error: width of &lsquo;a&rsquo; exceeds its type</font></p>
<p><b>二、位域的位序</b></p>
<p>再回顾一下上一节的最后那个例子（不使用匿名0长度位域时）：</p>
<p>&nbsp;<font face="Courier New">struct short_flag_t s;<br />
	&nbsp;memset(&amp;s, 0, sizeof(s));<br />
	&nbsp;s.a = 2; /* 10 */<br />
	&nbsp;s.b = 4; /* 100 */</font></p>
<p>dump bits的结果为<font face="Courier New">0100 1000 0000 0000</font>。</p>
<p>怎么感觉输出的结果与s.a和s.b的值对不上啊！根据a和b的值，dump bits的输出似乎应该为<font face="Courier&lt;br /&gt;<br />
      New">1010 0000 0000 0000</font>。对比这两个dump结果不同的部分：1010 0000 vs. 0100 1000，a和b的bit顺序恰好相反。之前一直与<a href="http://tonybai.com/2005/09/28/also-talk-about-byte-order/">字节序</a>做斗争，难不成bit也有序之分？事实就是这样的。bit也有order的概念，称为位序。位域字 段的内存位排序就称为该位域的位序。</p>
<p>我们来回顾一下字节序的概念，字节序分大端(big-endian，典型体系Sun Sparc)和小端(little-endian，典型体系Intel x86)：<br />
	大端指的是数值（比如0&#215;12345678）的逻辑最高位(0&#215;12)放在起始地址（低地址）上，简称高位低址，就是<b>高位放在起始地址</b>。<br />
	小端指的是数值（比如0&#215;12345678）的逻辑最低位(0&#215;78)放在起始地址（低地址）上，简称低位低址，就是<b>低位放在起始地址</b>。</p>
<p><font face="Courier New">看下面例子：</font></p>
<p><font face="Courier New">int<br />
	main()<br />
	{<br />
	&nbsp;&nbsp;&nbsp; char c[4];<br />
	&nbsp;&nbsp;&nbsp; unsigned int i = 0&#215;12345678;<br />
	&nbsp;&nbsp;&nbsp; memcpy(c, &amp;i, sizeof(i));</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; printf(&quot;%p &#8211; 0x%x\n&quot;, &amp;c[0], c[0]);<br />
	&nbsp;&nbsp;&nbsp; printf(&quot;%p &#8211; 0x%x\n&quot;, &amp;c[1], c[1]);<br />
	&nbsp;&nbsp;&nbsp; printf(&quot;%p &#8211; 0x%x\n&quot;, &amp;c[2], c[2]);<br />
	&nbsp;&nbsp;&nbsp; printf(&quot;%p &#8211; 0x%x\n&quot;, &amp;c[3], c[3]);<br />
	}</font></p>
<p>在x86 (小端机器)上输出结果如下：</p>
<p><font face="Courier New">0x7fff1a6747c0 &#8211; 0&#215;78<br />
	0x7fff1a6747c1 &#8211; 0&#215;56<br />
	0x7fff1a6747c2 &#8211; 0&#215;34<br />
	0x7fff1a6747c3 &#8211; 0&#215;12</font></p>
<p>在sparc(大端机器)上输出结果如下：</p>
<p><font face="Courier New">ffbffbd0 &#8211; 0&#215;12<br />
	ffbffbd1 &#8211; 0&#215;34<br />
	ffbffbd2 &#8211; 0&#215;56<br />
	ffbffbd3 &#8211; 0&#215;78</font></p>
<p>通过以上输出结果可以看出，小端机器的数值低位0&#215;78放在了低地址0x7fff1a6747c0上；而大端机器则是将数值高位0&#215;12放在了低 地址0xffbffbd0上。</p>
<p>机器的最小寻址单位是字节，bit无法寻址，也就没有高低地址和起始地址的概念，我们需要定义一下bit的&ldquo;地址&rdquo;。以一个字节为例，我们把从左到右的8个bit的位置(position)命名按顺序命名如下：</p>
<p><font face="Courier New">p7 p6 p5 p4 p3 p2 p1 p0</font></p>
<p>其中最左端的p7为起始地址。这样以<b>一字节大小</b>的数值10110101(b)为例，其在不同平台下的内存位序如下：</p>
<p>大端的含义是数值的最高位1（最左边的1）放在了起始位置p7上，即数值10110101的大端内存布局为10110101。<br />
	小端的含义是数值的最低位1(最右边的1)放在了起始位置p7上，即数值10110101的小端内存布局为10101101。</p>
<p>前面的函数dump_native_bits_storage_layout也是符合这一定义的，即最左为起始位置。</p>
<p>同理，对于一个bit个数为3且存储的数值为110(b)的位域而言，将其3个bit的位置按顺序命名如下：</p>
<p><font face="Courier New">p2 p1 p0</font></p>
<p>其在大端机器上的bit内存布局，即位域位序为： <font face="Courier New">110</font>;<br />
	其在小端机器上的bit内存布局，即位域位序为： <font face="Courier New">011</font>。</p>
<p>在此基础上，理解上面例子中的疑惑就很简单了。</p>
<p>&nbsp;s.a = 2; /* 10(b) ，大端机器上位域位序为 10，小端为01 */<br />
	&nbsp;s.b = 4; /* 100(b)，大端机器上位域位序为100，小端为001 */</p>
<p>于是在x86（小端）上的dump bits结果为：<font face="Courier New">0100 1000 0000 0000</font><br />
	而在sparc(大端）上的dump bits结果为：<font face="Courier New">1010 0000 0000 0000</font></p>
<p>同时我们可以看出这里是根据位域进行单独赋值的，这样<b>位域的位序是也是以位域为单位排列的</b><b>，即每个位域内部独立排序</b>， 而不是按照存储单元（这里的存储单元是16bit）或按字节内bit序排列的。</p>
<p><b>三、tcphdr定义分析</b></p>
<p>前面提到过在linux-kernel.cn mail list中的那个问题大致如下：</p>
<p>tcphdr定义中的大端代码：</p>
<p><font face="Courier New">__u16&nbsp;&nbsp; doff:4,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; res1:4,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; cwr:1,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ece:1,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; urg:1,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ack:1,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; psh:1,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; rst:1,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; syn:1,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fin:1;</font></p>
<p><font face="Courier New">问题是其对应的小端代码该如何做字段排序？似乎有两种方案摆在面前：</font></p>
<p>方案1:<br />
	<font face="Courier New"><font face="Courier New">__u16&nbsp;&nbsp;&nbsp; res1:4,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; doff:4,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fin:1,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; syn:1,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; rst:1,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; psh:1,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ack:1,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; urg:1,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ece:1,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; cwr:1;</font></font></p>
<p><font face="Courier New"><font face="Courier New">or</font></font></p>
<p><font face="Courier New">方案2:<br />
	__u16&nbsp;&nbsp; cwr:1,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ece:1,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; urg:1,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ack:1,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; psh:1,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; rst:1,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; syn:1,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fin:1,<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; res1:4<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; doff:4;</font></p>
<p><font face="Courier New">个人觉得这两种方案从理论上都是没错的，关键还是看tcphdr是如何进行pack的，是按__u16整体打包，还是按byte打包。原代码中使用的是方 案1，推测出tcphdr采用的是按byte打包的方式，这样我们只需调换byte内的bit顺序即可。res1和doff是一个字节内的两个位域，如果 按自己打包，他们两个的顺序对调即可在不同端的平台上得到相同的结果。用下面实例解释一下：</font></p>
<p><font face="Courier New">假设在大端系统上，doff和res1的值如下：</font></p>
<p><font face="Courier New">doff res1<br />
	1100 1010 大端</font></p>
<p><font face="Courier New">在大端系统上pack后，转化为网络序：</font></p>
<p><font face="Courier New">doff res1<br />
	1100 1010 网络序</font></p>
<p><font face="Courier New">小端系统接收后，转化为本地序：</font></p>
<p><font face="Courier New">0101 0011</font></p>
<p><font face="Courier New">很显然，我们应该按如下方法对应：</font></p>
<p><font face="Courier New">res1 doff<br />
	0101 0011</font></p>
<p><font face="Courier New">也就相当于将doff和res1的顺序对调，这样在小端上依旧可以得到相同的值。</font></p>
<p style='text-align:left'>&copy; 2013, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2013/05/21/talk-about-bitfield-in-c-again/feed/</wfw:commentRss>
		<slash:comments>5</slash:comments>
		</item>
		<item>
		<title>libiconv库链接问题一则</title>
		<link>https://tonybai.com/2013/04/25/a-libiconv-linkage-problem/</link>
		<comments>https://tonybai.com/2013/04/25/a-libiconv-linkage-problem/#comments</comments>
		<pubDate>Thu, 25 Apr 2013 10:04:34 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[GCC]]></category>
		<category><![CDATA[Glibc]]></category>
		<category><![CDATA[GNU]]></category>
		<category><![CDATA[iconv]]></category>
		<category><![CDATA[ld]]></category>
		<category><![CDATA[libc]]></category>
		<category><![CDATA[libiconv]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[Redhat]]></category>
		<category><![CDATA[Solaris]]></category>
		<category><![CDATA[Unix]]></category>
		<category><![CDATA[博客]]></category>
		<category><![CDATA[工作]]></category>
		<category><![CDATA[开源]]></category>
		<category><![CDATA[程序员]]></category>
		<category><![CDATA[编译器]]></category>
		<category><![CDATA[链接]]></category>
		<category><![CDATA[链接器]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=1258</guid>
		<description><![CDATA[与在Solaris系统上不同，Linux的libc库中包含了libiconv库中函数的定义，因此在Linux上使用libiconv库相关函数，编译时是不需要显式-liconv的。但最近我的一位同事在某redhat enterprise server 5.6机器上编译程序时却遇到了找不到iconv库函数符号的链接问题，到底是怎样一回事呢？这里分享一下问题查找过程。 一、现场重现 这里借用一下这位同事的测试程序以及那台机器，重现一下问题过程： /*test.c */ &#8230; #include &#60;iconv.h&#62; int main(void) { &#160;&#160;&#160; int r; &#160;&#160;&#160; char *sin, *sout; &#160;&#160;&#160; size_t lenin, lenout; &#160;&#160;&#160; char *src = &#34;你好!&#34;; &#160;&#160;&#160; char dst[256] = {0}; &#160;&#160;&#160; iconv_t c_pt;&#160;&#160; &#160;&#160;&#160; sin = src; &#160;&#160;&#160; lenin = strlen(src)+1; &#160;&#160;&#160; sout = dst; &#160;&#160;&#160; lenout = 256; &#160;&#160;&#160; [...]]]></description>
			<content:encoded><![CDATA[<p>与在<a href="http://tonybai.com/2009/11/05/a-64bit-compiling-problem-on-x86-solaris/">Solaris</a>系统上不同，<a href="http://tonybai.com/2012/12/04/upgrade-ubuntu-to-1204-lts/">Linux</a>的libc库中包含了<a href="http://tonybai.com/2009/10/31/internal-code-transform-by-iconv/">libiconv</a>库中函数的定义，因此在Linux上使用libiconv库相关函数，编译时是不需要显式-liconv的。但最近我的一位同事在某redhat enterprise server 5.6机器上编译程序时却遇到了找不到iconv库函数符号的<a href="http://tonybai.com/2007/12/08/those-things-about-symbol-linkage/">链接问题</a>，到底是怎样一回事呢？这里分享一下问题查找过程。</p>
<p><b>一、现场重现</b></p>
<p>这里借用一下这位同事的测试程序以及那台机器，重现一下问题过程：<br />
	/*test.c */</p>
<p>&#8230;<br />
	<font face="Courier New">#include &lt;iconv.h&gt;</font></p>
<p><font face="Courier New">int main(void)<br />
	{<br />
	&nbsp;&nbsp;&nbsp; int r;<br />
	&nbsp;&nbsp;&nbsp; char *sin, *sout;<br />
	&nbsp;&nbsp;&nbsp; size_t lenin, lenout;<br />
	&nbsp;&nbsp;&nbsp; char *src = &quot;你好!&quot;;<br />
	&nbsp;&nbsp;&nbsp; char dst[256] = {0};<br />
	&nbsp;&nbsp;&nbsp; iconv_t c_pt;&nbsp;&nbsp;</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; sin = src;<br />
	&nbsp;&nbsp;&nbsp; lenin = strlen(src)+1;</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; sout = dst;<br />
	&nbsp;&nbsp;&nbsp; lenout = 256;</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; if ((c_pt = iconv_open(&quot;UTF-8&quot;, &quot;GB2312&quot;)) == (iconv_t)(-1)){<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; printf(&quot;iconv_open error!. errno[%d].\n&quot;, errno);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return -1;<br />
	&nbsp;&nbsp;&nbsp; }</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; if ((r = iconv(c_pt, (char **)&amp;sin, &amp;lenin, &amp;sout, &amp;lenout)) != 0){<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; printf(&quot;iconv error!. errno[%d].\n&quot;, r);<br />
	&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; return -1;<br />
	&nbsp;&nbsp;&nbsp; }&nbsp;&nbsp;</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; iconv_close(c_pt);</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; printf(&quot;SRC[%s], DST[%s].\n&quot;, src, dst);</font></p>
<p><font face="Courier New">&nbsp;&nbsp;&nbsp; return 0;<br />
	}</font></p>
<p>根据之前的经验，我们按如下命令编译该程序：</p>
<p><font face="Courier New">$&gt; gcc -g -o test test.c</font></p>
<p><font face="Courier New">/tmp/ccyQ5blC.o: In function `main&#39;:<br />
	/home/tonybai/tmp/test.c:28: undefined reference to `libiconv_open&#39;<br />
	/home/tonybai/tmp/test.c:33: undefined reference to `libiconv&#39;<br />
	/home/tonybai/tmp/test.c:38: undefined reference to `libiconv_close&#39;</font></p>
<p>咦，这是咋搞的呢？怎么找不到iconv库的符号！！！显式加上iconv的链接指示再试试。</p>
<p><font face="Courier New">$&gt; gcc -g -o test test.c -liconv</font></p>
<p>这回编译OK了。的确如那位同事所说出现了怪异的情况。</p>
<p><b>二、现场取证</b></p>
<p>惯性思维让我<b>首先</b>提出疑问：难道是这台机器上的<a href="http://www.gnu.org/s/libc/">libc</a>版本有差异，检查一下<a href="http://tonybai.com/2009/04/11/glibc-strlen-source-analysis/">libc</a>中是否定义了iconv相关符号。</p>
<p><font face="Courier New">$ nm /lib64/libc.so.6 |grep iconv<br />
	000000397141e040 T iconv<br />
	000000397141e1e0 T iconv_close<br />
	000000397141ddc0 T iconv_open</font></p>
<p>iconv的函数都定义了呀！怎么会链接不到？</p>
<p>我们<b>再来</b>看看已经编译成功的那个test到底连接到哪个iconv库了。</p>
<p><font face="Courier New">$ ldd test<br />
	&nbsp;&nbsp;&nbsp; linux-vdso.so.1 =&gt;&nbsp; (0x00007fff77d6b000)<br />
	&nbsp;&nbsp;&nbsp; libiconv.so.2 =&gt; /usr/local/lib/libiconv.so.2 (0x00002abbeb09e000)<br />
	&nbsp;&nbsp;&nbsp; libc.so.6 =&gt; /lib64/libc.so.6 (0&#215;0000003971400000)<br />
	&nbsp;&nbsp;&nbsp; /lib64/ld-linux-x86-64.so.2 (0&#215;0000003971000000)</font></p>
<p>哦，系统里居然在/usr/local/lib下面单独安装了一份libiconv。gcc显然是链接到这里的libiconv了，但gcc怎么会链接到这里了呢？</p>
<p><b>三、</b><b>大侦探的分析^_^</b></p>
<p><a href="http://tonybai.com/2006/03/14/explain-gcc-warning-options-by-examples/">Gcc</a>到底做了什么呢？我们看看其verbose的输出结果。</p>
<p><font face="Courier New">$ gcc -g -o test test.c -liconv -v<br />
	使用内建 specs。<br />
	目标：x86_64-redhat-linux<br />
	配置为：../configure &#8211;prefix=/usr &#8211;mandir=/usr/share/man &#8211;infodir=/usr/share/info &#8211;enable-shared &#8211;enable-threads=posix &#8211;enable-&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; checking=release &#8211;with-system-zlib &#8211;enable-__cxa_atexit &#8211;disable-libunwind-exceptions &#8211;enable-libgcj-multifile &#8211;enable-languages=c,c++,&nbsp;&nbsp; objc,obj-c++,java,fortran,ada &#8211;enable-java-awt=gtk &#8211;disable-dssi &#8211;disable-plugin &#8211;with-java-home=/usr/lib/jvm/java-1.4.2-gcj-1.4.2.0/jre &#8211;with-cpu=generic &#8211;host=x86_64-redhat-linux<br />
	线程模型：posix<br />
	gcc 版本 4.1.2 20080704 (Red Hat 4.1.2-50)<br />
	&nbsp;/usr/libexec/gcc/x86_64-redhat-linux/4.1.2/cc1 -quiet -v test.c -quiet -dumpbase test.c -mtune=generic -auxbase test -g -version -o /tmp/&nbsp;&nbsp;&nbsp;&nbsp; ccypZm0v.s<br />
	忽略不存在的目录&ldquo;/usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../x86_64-redhat-linux/include&rdquo;<br />
	#include &quot;&#8230;&quot; 搜索从这里开始：<br />
	#include &lt;&#8230;&gt; 搜索从这里开始：<br />
	&nbsp;/usr/local/include<br />
	&nbsp;/usr/lib/gcc/x86_64-redhat-linux/4.1.2/include<br />
	&nbsp;/usr/include<br />
	搜索列表结束。<br />
	GNU C 版本 4.1.2 20080704 (Red Hat 4.1.2-50) (x86_64-redhat-linux)<br />
	&nbsp;&nbsp;&nbsp; 由 GNU C 版本 4.1.2 20080704 (Red Hat 4.1.2-50) 编译。<br />
	GGC 准则：&#8211;param ggc-min-expand=100 &#8211;param ggc-min-heapsize=131072<br />
	Compiler executable checksum: ef754737661c9c384f73674bd4e06594<br />
	&nbsp;as -V -Qy -o /tmp/ccaqvDgX.o /tmp/ccypZm0v.s<br />
	GNU assembler version 2.17.50.0.6-14.el5 (x86_64-redhat-linux) using BFD version 2.17.50.0.6-14.el5 20061020<br />
	&nbsp;/usr/libexec/gcc/x86_64-redhat-linux/4.1.2/collect2 &#8211;eh-frame-hdr -m elf_x86_64 &#8211;hash-style=gnu -dynamic-linker /lib64/ld-linux-x86-64.so.&nbsp; 2 -o test /usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../lib64/crt1.o /usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../lib64/crti.o /usr/&nbsp;&nbsp; lib/gcc/x86_64-redhat-linux/4.1.2/crtbegin.o -L/usr/lib/gcc/x86_64-redhat-linux/4.1.2 -L/usr/lib/gcc/x86_64-redhat-linux/4.1.2 -L/usr/lib/gcc/ x86_64-redhat-linux/4.1.2/../../../../lib64 -L/lib/../lib64<br />
	-L/usr/lib/../lib64 /tmp/ccaqvDgX.o -liconv -lgcc &#8211;as-needed -lgcc_s &#8211;no-as-needed -lc -lgcc &#8211;as-needed -lgcc_s &#8211;no-as-needed /usr/lib/gcc/x86_64-redhat-linux/4.1.2/crtend.o /usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../lib64/crtn.o</font></p>
<p>从这个结果来看，gcc在search iconv.h这个头文件时，首先找到的是/usr/local/include/iconv.h，而不是/usr/include/iconv.h。这两个文件有啥不同么？</p>
<p>在/usr/local/include/iconv.h中，我找到如下代码：</p>
<p><font face="Courier New">&#8230;</font><br />
	<font face="Courier New">#ifndef LIBICONV_PLUG<br />
	#define iconv_open libiconv_open<br />
	#endif<br />
	extern iconv_t iconv_open (const char* tocode, const char* fromcode);<br />
	&#8230;</font></p>
<p>libiconv_open vs iconv_open，卧槽！！！再对比一下前面编译时输出的错误信息：</p>
<p><font face="Courier New">/tmp/ccyQ5blC.o: In function `main&#39;:<br />
	/home/tonybai/tmp/test.c:28: undefined reference to `libiconv_open&#39;<br />
	/home/tonybai/tmp/test.c:33: undefined reference to `libiconv&#39;<br />
	/home/tonybai/tmp/test.c:38: undefined reference to `libiconv_close&#39;</font></p>
<p>大侦探醒悟了！大侦探带你还原一下真实情况。</p>
<p>我们在执行<font face="Courier New">gcc -g -o test test.c</font>时， 根据gcc -v中include search dir的顺序，gcc首先search到的是/usr/local/include/iconv.h，而这里iconv_open等函数被预编译器替换成 了libiconv_open等加上了lib前缀的函数，而这些函数符号显然在libc中是无法找到的，libc中只有不带lib前缀的 iconv_open等函数的定义。大侦探也是一时眼拙了，没有细致查看gcc的编译错误信息中的内容，这就是问题所在！</p>
<p>而<font face="Courier New">gcc -g -o test test.c -liconv</font>为何可以顺利编译通过呢？gcc是如何找到/usr/local/lib下的libiconv的呢？大侦探再次为大家还原一下真相。</p>
<p>我们在执行<font face="Courier New">gcc -g -o test test.c -liconv</font>时，gcc同 样首先search到的是/usr/local/include/iconv.h，然后编译test.c源码，ok；接下来启动ld程序进行链接；ld找 到了libiconv，ld是怎么找到iconv的呢，libiconv在/usr/local/lib下，ld显然是到这个目录下search了。我们 通过执行下面命令可以知晓ld的默认搜索路径：</p>
<p><font face="Courier New">$&gt; ld -verbose|grep SEARCH<br />
	SEARCH_DIR(&quot;/usr/x86_64-redhat-linux/lib64&quot;); SEARCH_DIR(&quot;/usr/local/lib64&quot;); SEARCH_DIR(&quot;/lib64&quot;); SEARCH_DIR(&quot;/usr/lib64&quot;); SEARCH_DIR(&quot;/usr/x86_64-redhat-linux/lib&quot;); SEARCH_DIR(&quot;/usr/lib64&quot;); SEARCH_DIR(&quot;/usr/local/lib&quot;); SEARCH_DIR(&quot;/lib&quot;); SEARCH_DIR(&quot;/usr/lib&quot;);</font></p>
<p>ld的默认search路径中有/usr/local/lib(我之前一直是以为/usr/local/lib不是gcc/ld的默认搜索路径的)，因此找到libiconv就不足为奇了。</p>
<p><b>四、问题解决</b></p>
<p>我们不想显式的加上-liconv，那如何解决这个问题呢？我们是否可以强制gcc先找到/usr/include/iconv.h呢？我们先来做个试验。</p>
<p><font face="Courier New">$ gcc -g -o test test.c -liconv -I ~/include -v<br />
	&#8230;<br />
	忽略不存在的目录&ldquo;/usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../x86_64-redhat-linux/include&rdquo;<br />
	#include &quot;&#8230;&quot; 搜索从这里开始：<br />
	#include &lt;&#8230;&gt; 搜索从这里开始：<br />
	&nbsp;<b>/home/</b><b>tonybai</b><b>/include</b><br />
	&nbsp;/usr/local/include<br />
	&nbsp;/usr/lib/gcc/x86_64-redhat-linux/4.1.2/include<br />
	&nbsp;/usr/include<br />
	搜索列表结束。</font></p>
<p><font face="Courier New">&#8230;</font></p>
<p>试验结果似乎让我们觉得可行，我们通过-I指定的路径被放在了第一的位置进行search。我们来尝试一下强制gcc先search /usr/include。</p>
<p><font face="Courier New">$ gcc -g -o test test.c -I ~/include -v<br />
	&#8230;<br />
	忽略不存在的目录&ldquo;/usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../x86_64-redhat-linux/include&rdquo;<br />
	忽略重复的目录&ldquo;/usr/include&rdquo;<br />
	&nbsp; 因为它是一个重复了系统目录的非系统目录<br />
	#include &quot;&#8230;&quot; 搜索从这里开始：<br />
	#include &lt;&#8230;&gt; 搜索从这里开始：<br />
	&nbsp;/usr/local/include<br />
	&nbsp;/usr/lib/gcc/x86_64-redhat-linux/4.1.2/include<br />
	&nbsp;/usr/include<br />
	搜索列表结束。<br />
	&#8230;</font></p>
<p>糟糕！/usr/include被忽略了！还是从/usr/local/include开始，方案失败。</p>
<p>似乎剩下的唯一方案就是将/usr/local/lib下的那份libiconv卸载掉！那就这么做吧^_^！</p>
<p style='text-align:left'>&copy; 2013, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2013/04/25/a-libiconv-linkage-problem/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>玩转top</title>
		<link>https://tonybai.com/2013/03/02/deep-into-top/</link>
		<comments>https://tonybai.com/2013/03/02/deep-into-top/#comments</comments>
		<pubDate>Sat, 02 Mar 2013 10:07:59 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[CPU]]></category>
		<category><![CDATA[GCC]]></category>
		<category><![CDATA[GNU]]></category>
		<category><![CDATA[Kernel]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Opensource]]></category>
		<category><![CDATA[Performance]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[top]]></category>
		<category><![CDATA[Unix]]></category>
		<category><![CDATA[内核]]></category>
		<category><![CDATA[博客]]></category>
		<category><![CDATA[工作]]></category>
		<category><![CDATA[工具]]></category>
		<category><![CDATA[开源]]></category>
		<category><![CDATA[性能]]></category>
		<category><![CDATA[程序员]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=1207</guid>
		<description><![CDATA[相信很多人和我一样，top是自己日常使用最多的linux资源查看工具。不过仅限于一些简单的日常场景罢了：敲入top命令，看看哪些进程占用 CPU较多，然后对这些CPU占用较多的进程逐一处理一下。显然这样使用top有些大才小用了。 以前在监控工具使用方面总是浅尝辙止，并未做过多深入研究。近来愈来愈觉得有必要针对几种常用工具好好学习一下了。而top便首当其冲。top是一款 以查看进程(task)信息为中心的Linux系统性能监控工具，通过top我们可以查看到进程相关的cpu和内存占用相关的实时采样信息，因此 top尤其适合用于持续跟踪分析某些进程对系统cpu和内存的占用情况以及对系统负荷的影响。 入门 top的入门使用极其简单，就像前面所说的简单地的输入&#34;top&#34;，我们就能看到top的输出了。 top &#8211; 06:35:47 up 7 min,&#160; 3 users,&#160; load average: 1.00, 1.18, 0.67 Tasks: 189 total,&#160;&#160; 2 running, 186 sleeping,&#160;&#160; 0 stopped,&#160;&#160; 1 zombie Cpu(s): 30.5%us,&#160; 7.6%sy,&#160; 0.0%ni, 60.5%id,&#160; 1.5%wa,&#160; 0.0%hi,&#160; 0.0%si,&#160; 0.0%st Mem:&#160;&#160; 1534164k total,&#160; 1423392k used,&#160;&#160; 110772k free,&#160;&#160;&#160; 67328k buffers Swap:&#160;&#160; 999420k total,&#160;&#160;&#160;&#160;&#160; 144k used,&#160;&#160; 999276k [...]]]></description>
			<content:encoded><![CDATA[<p>相信很多人和我一样，top是自己日常使用最多的linux资源查看工具。不过仅限于一些简单的日常场景罢了：敲入top命令，看看哪些进程占用 CPU较多，然后对这些CPU占用较多的进程逐一处理一下。显然这样使用top有些大才小用了。</p>
<p>以前在监控工具使用方面总是浅尝辙止，并未做过多深入研究。近来愈来愈觉得有必要针对几种常用工具好好学习一下了。而top便首当其冲。top是一款 以查看进程(task)信息为中心的Linux系统性能监控工具，通过top我们可以查看到进程相关的cpu和内存占用相关的实时采样信息，因此 top尤其适合用于持续跟踪分析某些进程对系统cpu和内存的占用情况以及对系统负荷的影响。</p>
<p><b>入门</b></p>
<p>top的入门使用极其简单，就像前面所说的简单地的输入&quot;top&quot;，我们就能看到top的输出了。</p>
<p><span style="font-family:courier new,courier,monospace;">top &#8211; 06:35:47 up 7 min,&nbsp; 3 users,&nbsp; load average: 1.00, 1.18, 0.67<br />
	Tasks: 189 total,&nbsp;&nbsp; 2 running, 186 sleeping,&nbsp;&nbsp; 0 stopped,&nbsp;&nbsp; 1 zombie<br />
	Cpu(s): 30.5%us,&nbsp; 7.6%sy,&nbsp; 0.0%ni, 60.5%id,&nbsp; 1.5%wa,&nbsp; 0.0%hi,&nbsp; 0.0%si,&nbsp; 0.0%st<br />
	Mem:&nbsp;&nbsp; 1534164k total,&nbsp; 1423392k used,&nbsp;&nbsp; 110772k free,&nbsp;&nbsp;&nbsp; 67328k buffers<br />
	Swap:&nbsp;&nbsp; 999420k total,&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 144k used,&nbsp;&nbsp; 999276k free,&nbsp;&nbsp; 576924k cached</span></p>
<p>&nbsp; PID USER&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; PR&nbsp; NI&nbsp; VIRT&nbsp; RES&nbsp; SHR S %CPU %MEM&nbsp;&nbsp;&nbsp; TIME+&nbsp; COMMAND&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br />
	&nbsp;1954 tonybai&nbsp;&nbsp; 20&nbsp;&nbsp; 0&nbsp; 316m&nbsp; 55m&nbsp; 26m S&nbsp;&nbsp; 26&nbsp; 3.7&nbsp;&nbsp; 0:36.53 compiz&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br />
	&nbsp;2308 tonybai&nbsp;&nbsp; 20&nbsp;&nbsp; 0&nbsp; 499m&nbsp; 84m&nbsp; 39m S&nbsp;&nbsp; 13&nbsp; 5.6&nbsp;&nbsp; 1:07.63 chrome<br />
	&#8230; &#8230;</p>
<p>top的输出大致分为上下两个部分，上半部分输出到是系统的总体负荷信息，下半部分则是分进程列出进程的各种属性信息。</p>
<p>总体负荷信息由五行组成：</p>
<p>第一行：top &#8211; 06:35:47 up 7 min,&nbsp; 3 users,&nbsp; load average: 1.00, 1.18, 0.67。<br />
	这行的输出与uptime命令是一样一样的，不信你可以单独执行一下uptime命令。我怀疑top就是直接调用uptime或使用uptime部分代码 得到的，毕竟它们都是<a href="http://procps.sourceforge.net">procps</a>（procps is the package that has a bunch of small useful utilities that give information about processes using the /proc filesystem.）工具集合的一员。这行输出了当前时间( 06:35:47)、自系统启动以来的累计时间(7 min)，当前系统用户数(3 users)，1分钟，5分钟以及15分钟的<a href="http://blog.scoutapp.com/articles/2009/07/31/understanding-load-averages">平均负荷</a>( load average: 1.00, 1.18, 0.67)。</p>
<p>第二行：Tasks: 189 total,&nbsp;&nbsp; 2 running, 186 sleeping,&nbsp;&nbsp; 0 stopped,&nbsp;&nbsp; 1 zombie。<br />
	系统的进程信息汇总，包括总数以及处于各种状态的进程数量。</p>
<p>第三行：Cpu(s): 30.5%us,&nbsp; 7.6%sy,&nbsp; 0.0%ni, 60.5%id,&nbsp; 1.5%wa,&nbsp; 0.0%hi,&nbsp; 0.0%si,&nbsp; 0.0%st。<br />
	系统的CPU信息汇总，包括us(CPU用于运行用户空间进程的时间所占比例，不包括renice的用户进程)、sy(CPU用于运行内核进程的时间所占 比例)、ni(CPU用于运行用户空间被renice的进程的时间所占比例)、id（CPU空闲时间所占比例）、wa(CPU等待I/O完成时间所占用的 比例)、hi（处理硬件中断时间所占比例）、si(处理软中断时间所占比例)、st(虚拟机管理程序为其他task而从本虚拟机&#39;偷取&#39;的CPU时间所占 比例)。</p>
<p>第四行和第五行：<br />
	<span style="font-family:courier new,courier,monospace;">Mem:&nbsp;&nbsp; 1534164k total,&nbsp; 1423392k used,&nbsp;&nbsp; 110772k free,&nbsp;&nbsp;&nbsp; 67328k buffers<br />
	Swap:&nbsp;&nbsp; 999420k total,&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 144k used,&nbsp;&nbsp; 999276k free,&nbsp;&nbsp; 576924k cached</span></p>
<p>系统的内存以及交换区信息汇总，包括内存总量(mem total)、已使用内存(mem used)、空闲内存(mem free)以及交换区总量(swap total)、交换区使用量(swap used)、交换区空闲(swap free)。这里还有两个值buffers和cache，它们是内核使用的内存缓存，均是用于减少磁盘读取，提升系统性能的。buffers代表有多少内 存用于缓存磁盘数据块，目的是减少写磁盘次数；cache用于缓存从磁盘文件读取的数据，以减少读磁盘次数。</p>
<p>下半部分是进程属性信息展示区。默认情况输出的进程属性包括：<br />
	&nbsp;&nbsp;&nbsp; PID(进程ID)<br />
	&nbsp;&nbsp;&nbsp; USER(进程所有者的用户名)<br />
	&nbsp;&nbsp;&nbsp; PR（进程的动态优先级)<br />
	&nbsp;&nbsp;&nbsp; NI（Nice值，进程的base priority）<br />
	&nbsp;&nbsp;&nbsp; VIRT (进程的虚拟内存用量，包括进程的二进制映像大小、数据区以及所有加载的共享库占用的size， = SWAP + RES)<br />
	&nbsp;&nbsp;&nbsp; RES（进程使用的、未被换出的物理内存大小,= CODE + DATA)<br />
	&nbsp;&nbsp;&nbsp; SHR(共享内存区域大小)<br />
	&nbsp;&nbsp;&nbsp; S（进程状态)<br />
	&nbsp;&nbsp;&nbsp; %CPU（上次刷新到现在运行该task的CPU时间所占百分比）<br />
	&nbsp;&nbsp;&nbsp; %MEM（当前task所占用的内存百分比）<br />
	&nbsp;&nbsp;&nbsp; TIME+&nbsp; （自task启动后所使用的CPU时间累计）<br />
	&nbsp;&nbsp;&nbsp; COMMAND （task对应的二进制程序名）</p>
<p><b>定制输出</b></p>
<p>top提供了强大的输出定制功能，无论是上半部分的系统整体负荷信息还是下半部分的进程属性信息展示都是可以根据使用的需求定制的。</p>
<p>整体负荷信息展示区的定制：<br />
	- 第一行展示/隐藏：通过点击键盘上的&#39;l&#39;键可以展示或隐藏第一行信息输出<br />
	- Task和CPU信息展示/隐藏：通过点击键盘上的&#39;t&#39;键可以展示或隐藏Task和CPU行输出<br />
	- Mem和Swap信息展示/隐藏：通过点击键盘上的&#39;m&#39;键可以展示或隐藏Mem和Swap行输出</p>
<p>进程属性信息的显示定制：<br />
	默认情况下，我们可以看到top会显示进程的若干属性，包括PID、USER、PR、NI 、VIRT 、RES 、SHR、S、%CPU以及%MEM等。不过这些也仅仅是默认的而已，如果你不关住其中一些属性或关注其他一些属性，你完全可以自定义输出显示的进程属 性。点击键盘上的&#39;f&#39;键，top将为我们打开field选择页面：</p>
<p><span style="font-family:courier new,courier,monospace;">Current Fields:&nbsp; AEHIOQTWKNMbcdfgjplrsuvyzX&nbsp; for window 1:Def<br />
	Toggle fields via field letter, type any other key to return</span></p>
<p>* A: PID&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; = Process Id&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 0&#215;00002000&nbsp; PF_FREE_PAGES (2.5)<br />
	* E: USER&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; = User Name&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 0&#215;00008000&nbsp; debug flag (2.5)<br />
	* H: PR&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; = Priority&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 0&#215;00024000&nbsp; special threads (2.5)<br />
	&#8230; &#8230;</p>
<p>页面左侧列出了可选的所有进程属性。其中前面有*前缀的是当前已经选择的属性，比如PID。不过你可以通过点击PID对应的开关键&#39;A&#39;来取消对PID的 选择；同样你也可以点击未选择属性前面的开关键来选择对应的属性，比如敲击&#39;p&#39;来选择SWAP属性。定制完毕后回车回到top主页面，你就会看到你定制 后的结果了。</p>
<p><b>保存你的定制</b></p>
<p>如果你不想每次都在top启动后重新做定制操作，那就将你的定制保存到top的用户配置文件中。在定制后的top主页面上输入：&#39;W&#39;，top会提示你：Wrote configuration to &#39;/home/tonybai/.toprc，也就是说top会将你的定制保存在你的~/.toprc中。重启top看看，是否依旧是上次你定制后的结果呢^_^。</p>
<p><b>多视图</b></p>
<p>默认情况下top为我们打开了一个视图。不过top可不止支持一个视图。敲入&#39;A&#39;看看会发生什么？没错，你会看到上下分割的四副视图，另外在整个窗口的 左上角会出现反白的&#39;1:Def&#39;，这是一个active视图的提示文字。反复输入&#39;w&#39;，top会在各个视图间切换，左上角会在&#39;1:Def&#39;、 &#39;2:Job&#39;、&#39;3:Mem&#39;和&#39;4:Usr&#39;之间切换。&lsquo;1:Def&#39;是默认视图，以CPU占用高低对task进行排序；&#39;2:Job&#39;这个视图看起 来比较陌生，里面展示的task多是些系统服务或内核线程；&#39;3:Mem&#39;视图则是以Mem占用高低对task进行排序；&#39;4:Usr&#39;视图则是按用户名 展示task。用&#39;w&#39;切换到某个视图后，可以输入&#39;A&#39;将该active视图放大为单视图铺满窗口。在多视图展示的情况下，还可以输入&#39;-&#39;来隐藏/展 示某种视图。另外这种多视图的配置也是可以保存在.toprc中的。</p>
<p><b>批处理模式</b></p>
<p>平时我们更多用的是在交互模式下运行的top，但交互模式下的数据无法记录下来，不便于事后分析，不过top的批处理模式可弥补这一不足。</p>
<p>执行top -b，即可让top以批处理模式运行。默认情况下top会不断重复执行，似乎批处理模式意义不大。不过我们可以限定批处理模式的运行间隔和运行次数，默认情况下top运行/更新间隔为3s，运行次数为无限制。我们可以通过一些命令行参数来设定这两个值，比如：</p>
<p><span style="font-family:courier new,courier,monospace;">$&gt; top -b&nbsp; -d 1 -n 10</span></p>
<p>-d 用来设置更新间隔为1s；而-n 则设置批处理运行10次。</p>
<p>默认情况下top输出的task太多，我们可以通过指定相关进程或指定user来将关注面缩小，比如：</p>
<p><span style="font-family:courier new,courier,monospace;">$&gt; top -b -p 2500 -p 2501 -d 1 -n 10</span></p>
<p>这个命令只是会输出2500和2501这两个进程的相关信息。</p>
<p><span style="font-family:courier new,courier,monospace;">$&gt; top -b -u www-data -d 1 -n 10</span></p>
<p>这个命令只会输出www-data这个用户下的所有进程相关信息。</p>
<p>即便在批处理模式下，top依旧会输出整体负荷信息。这样一来对后续的数据后处理会带来些麻烦。一个好的方法是先定制top，再做批处理执行。比如先用 l,m,t把top的整体负荷信息都关闭掉，再定制好要关注的进程属性，保存到toprc中；之后再批处理运行top（可将输出结果重定向到某个数据文件 中），我们得到的数据就会比较规整，处理起来也十分方便了。</p>
<p style='text-align:left'>&copy; 2013, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2013/03/02/deep-into-top/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>Go defer的C实现</title>
		<link>https://tonybai.com/2013/02/03/implement-go-defer-in-c/</link>
		<comments>https://tonybai.com/2013/02/03/implement-go-defer-in-c/#comments</comments>
		<pubDate>Sun, 03 Feb 2013 15:48:52 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Blogger]]></category>
		<category><![CDATA[C]]></category>
		<category><![CDATA[defer]]></category>
		<category><![CDATA[GCC]]></category>
		<category><![CDATA[GNU]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Opensource]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[博客]]></category>
		<category><![CDATA[开源]]></category>
		<category><![CDATA[思考]]></category>
		<category><![CDATA[程序员]]></category>
		<category><![CDATA[编译器]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=1189</guid>
		<description><![CDATA[Go语言中引入了一个新的关键字defer，个人认为这个语法关键字让异常处理也变得得心应手许多，对改善代码的可读性和可维护性大有裨益，是典型的语法棒棒糖^_^。 像下面这种代码（伪代码）： void foo() { &#160; &#160; apply resource1; &#160; &#160; retv = action1; &#160; &#160; if not success &#160; &#160; &#160; &#160; release resource1 &#160; &#160; apply resource2; &#160; &#160; retv = action2; &#160; &#160; if not success &#160; &#160; &#160; &#160; release resource1 &#160; &#160; &#160; &#160; release resource2 } 有了defer后，代码就变得优美多了。 void foo_with_defer() [...]]]></description>
			<content:encoded><![CDATA[<p><a href="http://golang.org">Go语言</a>中引入了一个新的关键字defer，个人认为这个语法关键字让异常处理也变得得心应手许多，对改善代码的可读性和可维护性大有裨益，是典型的语法棒棒糖^_^。</p>
<p>像下面这种代码（伪代码）：</p>
<p><span style="font-family:courier new,courier,monospace;">void foo() {<br />
	&nbsp; &nbsp; apply resource1;</span></p>
<p><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; retv = action1;<br />
	&nbsp; &nbsp; if not success<br />
	&nbsp; &nbsp; &nbsp; &nbsp; release resource1</span></p>
<p><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; apply resource2;</span></p>
<p><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; retv = action2;<br />
	&nbsp; &nbsp; if not success<br />
	&nbsp; &nbsp; &nbsp; &nbsp; release resource1<br />
	&nbsp; &nbsp; &nbsp; &nbsp; release resource2<br />
	}</span></p>
<p>有了defer后，代码就变得优美多了。</p>
<p><span style="font-family:courier new,courier,monospace;">void foo_with_defer() {<br />
	&nbsp; &nbsp; apply resource1;<br />
	&nbsp; &nbsp; defer (release_resource1)</span></p>
<p><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; retv = action1;<br />
	&nbsp; &nbsp; if not success<br />
	&nbsp; &nbsp; &nbsp; &nbsp; return</span></p>
<p><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; apply resource2;<br />
	&nbsp; &nbsp; defer (release_resource2)</span></p>
<p><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; retv = action2;<br />
	&nbsp; &nbsp; if not success<br />
	&nbsp; &nbsp; &nbsp; &nbsp; return<br />
	}</span></p>
<p>如果能在<a href="http://tonybai.com/tag/C">C语言</a>中实现defer这样的语法糖，那该多棒！是否可行呢？经过一段时间钻研，找到一个不那么美的实现方法，约束也很多，也不甚严谨， 谈不上什么可移植性，切不可用到产品环境，权当一种探讨罢了。</p>
<p>Go中defer的语义大致是这样的：<br />
	* 在使用defer的函数退出前，defer后面的函数将会被执行；<br />
	* 如果一个函数内有多个defer，那么defer按后进先出（LIFO）的顺行执行；<br />
	* 即使发生Panic，defer依然可以得到执行</p>
<p>最后一个比较难于模拟，这里仅先尝试前两个语义。下面从设计思路说起。</p>
<p><strong>* &ldquo;借东风&rdquo;</strong></p>
<p>要想模拟defer，首先要考虑的一点那就是defer后的语句是在函数return之前执行的。在标准C中，我们无任何举措可以实现这些。要在 C中实现defer，势必要借用一些编译器扩展特性，比如Gcc的扩展。这里实验所使用的编译器是<a href="http://tonybai.com/2006/03/14/explain-gcc-warning-options-by-examples/">Gcc</a>(4.6.3 (<a href="http://tonybai.com/2012/12/04/upgrade-ubuntu-to-1204-lts/">Ubuntu 12.04</a>))。Gcc扩展支持-finstrument-functions编译选项，该选项可以在函数执行前后插入一段运行代码。在之前写过的一篇名 为&ldquo;<a href="http://tonybai.com/2011/07/13/add-enter-and-exit-trace-for-your-function/">为函数添加enter和exit级trace</a>&rdquo;的文章中对此有较为详细的说明，这里我们还要用到这个扩展特性。</p>
<p><strong>* 偷天换日</strong></p>
<p>如果完全模仿Go的语法，在C中使用defer，大致是这样一种形式：</p>
<p><span style="font-family:courier new,courier,monospace;">void foo(void) {<br />
	&nbsp; &nbsp; FILE * fp = NULL;<br />
	&nbsp; &nbsp; fp = fopen(&quot;foo.txt&quot;, &quot;r&quot;);<br />
	&nbsp; &nbsp; if (!fp) return;<br />
	&nbsp; &nbsp; defer(fclose(fp));<br />
	&nbsp; &nbsp;<br />
	&nbsp; &nbsp; /* use fp */<br />
	&nbsp; &nbsp; &#8230; &#8230;<br />
	&nbsp; &nbsp; return;<br />
	}</span></p>
<p>但C毕竟是C，一门静态的编译型语言，我们如何将fclose(fp)这个信息传递给编译器自动插入的代码中呢？在C语言中，几乎没有手段获得函 数的元信息以及运行时参数信息，并再通过这些信息重新调用和执行该函数。我们得&ldquo;想招&rdquo;将这些信息存储起来。</p>
<p>大家知道C语言中的函数，比如这里的fclose，其实是一个函数起始地址；如果我们知道函数地址或又叫函数指针，再加上函数的参数，我们就可以 拼凑在一起执行该函数了。但理论上来说，函数指针也是有类型的，比如：</p>
<p><span style="font-family:courier new,courier,monospace;">typedef int (*FUNC_POINTER)(int, int);</span></p>
<p>这个函数指针类型可以用来执行诸如：int foo(int a, int b)这样的函数，比如：</p>
<p><span style="font-family:courier new,courier,monospace;">FUNC_POINTER fp = foo；<br />
	fp(1, 2);</span></p>
<p>但defer后面执行的函数千差万别，我们如何能够得知函数对应的函数指针类型呢？用void*存储？比如：</p>
<p><span style="font-family:courier new,courier,monospace;">void *p = foo;<br />
	p(1, 2);</span></p>
<p>编译器会给你一个严重错误！p不是函数指针，不能这么用。那我们如何能让编译器知道这个指针是一个可调用的函数指针呢？我们试试来定义一个&ldquo;通用 的函数指针&rdquo;：</p>
<p><span style="font-family:courier new,courier,monospace;">typedef void (*defer_func)();</span></p>
<p>没有返回值，没有参数，这样的函数指针能否执行foo这样的函数呢？答案是可以的，但不是那么完美。至少你不会得到返回值。这么做有两点考虑：<br />
	a) 至少可以让编译器知道这是一个函数指针，可以被用来执行函数。<br />
	b) 通常我们并不关心defer后面函数的返回值。<br />
	c) 参数列表的不同至少目前可以逃过编译器的错误检查，至多给个Warning。</p>
<p>函数指针的问题暂时算是有着落了，那参数怎么办？也就是说defer(fclose(fp))中的fp如何存储下来呢？如果在C中真的使用 defer(fclose(p))这种形式的语法，那么我是砸破脑袋也想不出啥招了！因此我们应该重新设计一下C中的defer应该如何使用？我 们用下面的语法来替代：</p>
<p><span style="font-family:courier new,courier,monospace;">defer(fclose, 1, p);</span></p>
<p>fclose是函数起始地址，1是参数个数，p则是传给fclose的参数。这样fclose和p都可以单独分离出来存储了。但是还是那句 话：defer后面可以执行的函数千万种，哪能穷尽？怎么才能表示成一种通用的方式存储参数呢？回想一下自己在编码过程中用于释放资源的那几类函 数，无非就是关闭文件、关闭文件描述符(包括socket)、释放内存等，这些函数传递的参数不是指针就是整型数，少有传浮点类型或将一个自定义 结构体以传值的方式传入的。我们不妨再次尝试一次&ldquo;偷天换日&rdquo; &#8211; 用void*存储整型参数或任意指针类型参数。当然其约束就像刚才所说的那些。不过对付大多数资源释放函数而言，应该是足够的了。至于将参数个数也作为一 个固定参数放入defer中，也是鉴于目前无法通过操作可变个数参数列表相关宏来获得参数数量。</p>
<p>最后一个问题。由于被defer的函数的参数个数不定。defer无法将<a href="http://tonybai.com/2008/05/07/also-talk-about-c-variable-length-args/">可变个数参数</a>重组后传给被defer的函数。因此目前暂只能通过一种&ldquo;丑陋&rdquo;的方式来实现。样例中最多只支持两个参数的被defer函数。</p>
<p><strong>* 样例</strong></p>
<p>首先看看我们的examples的主函数文件main.c。</p>
<p><span style="font-family:courier new,courier,monospace;">#include &lt;stdio.h&gt;<br />
	#include &lt;stdlib.h&gt;<br />
	#include &quot;defer.h&quot;</span></p>
<p><span style="font-family:courier new,courier,monospace;">int bar(int a, char *s) {<br />
	&nbsp; &nbsp; printf(&quot;a = [%d], s = [%s]\n&quot;, a, s);<br />
	}</span></p>
<p><span style="font-family:courier new,courier,monospace;">int main() {<br />
	&nbsp; &nbsp; FILE *fp = NULL;<br />
	&nbsp; &nbsp; fp = fopen(&quot;main.c&quot;, &quot;r&quot;);<br />
	&nbsp; &nbsp; if (!fp) return;<br />
	&nbsp; &nbsp; defer(fclose, 1, fp);</span></p>
<p><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; int *p = malloc(sizeof(*p));<br />
	&nbsp; &nbsp; if (!p) return;<br />
	&nbsp; &nbsp; defer(free, 1, p);</span></p>
<p><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; defer(bar, 2, 13, &quot;hello&quot;);<br />
	&nbsp; &nbsp; return 0;<br />
	}</span></p>
<p>从这里我们可以看到defer的用法，但这不是重点，重点是实现。</p>
<p>有了上面的一些设计思路的阐述，下面的代码也就不难理解了。核心是defer.c。<br />
	<span style="font-family:courier new,courier,monospace;">/* defer.h */<br />
	typedef void (*defer_func)();</span></p>
<p><span style="font-family:courier new,courier,monospace;">struct zero_params_func_ctx {<br />
	&nbsp; &nbsp; defer_func df;<br />
	};</span></p>
<p><span style="font-family:courier new,courier,monospace;">struct one_params_func_ctx {<br />
	&nbsp; &nbsp; defer_func df;<br />
	&nbsp; &nbsp; void *p1;<br />
	};</span></p>
<p><span style="font-family:courier new,courier,monospace;">struct two_params_func_ctx {<br />
	&nbsp; &nbsp; defer_func df;<br />
	&nbsp; &nbsp; void *p1;<br />
	&nbsp; &nbsp; void *p2;<br />
	};</span></p>
<p><span style="font-family:courier new,courier,monospace;">struct defer_func_ctx {<br />
	&nbsp; &nbsp; int params_count;<br />
	&nbsp; &nbsp; union {<br />
	&nbsp; &nbsp; &nbsp; &nbsp; struct zero_params_func_ctx zp;<br />
	&nbsp; &nbsp; &nbsp; &nbsp; struct one_params_func_ctx op;<br />
	&nbsp; &nbsp; &nbsp; &nbsp; struct two_params_func_ctx tp;<br />
	&nbsp; &nbsp; } ctx;<br />
	};</span></p>
<p><span style="font-family:courier new,courier,monospace;">void stack_push(struct defer_func_ctx *ctx);<br />
	struct defer_func_ctx* stack_pop();<br />
	int stack_top();</span></p>
<p><span style="font-family:courier new,courier,monospace;">/* defer.c */<br />
	struct defer_func_ctx ctx_stack[10];<br />
	int top_of_stack = 0; /* stack top from 1 to 10 */</span></p>
<p><span style="font-family:courier new,courier,monospace;">void stack_push(struct defer_func_ctx *ctx) {<br />
	&nbsp; &nbsp; if (top_of_stack &gt;= 10) {<br />
	&nbsp; &nbsp; &nbsp; &nbsp; return;<br />
	&nbsp; &nbsp; }</span></p>
<p><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; ctx_stack[top_of_stack] = *ctx;<br />
	&nbsp; &nbsp; top_of_stack++;<br />
	}</span></p>
<p><span style="font-family:courier new,courier,monospace;">struct defer_func_ctx* stack_pop() {<br />
	&nbsp; &nbsp; if (top_of_stack == 0) {<br />
	&nbsp; &nbsp; &nbsp; &nbsp; return NULL;<br />
	&nbsp; &nbsp; }</span></p>
<p><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; top_of_stack&#8211;;<br />
	&nbsp; &nbsp; return &amp;ctx_stack[top_of_stack];<br />
	}</span></p>
<p><span style="font-family:courier new,courier,monospace;">int stack_top() {<br />
	&nbsp; &nbsp; return top_of_stack;<br />
	}</span></p>
<p><span style="font-family:courier new,courier,monospace;">void defer(defer_func fp, int arg_count, &#8230;) {<br />
	&nbsp; &nbsp; va_list ap;<br />
	&nbsp; &nbsp; va_start(ap, arg_count);</span></p>
<p><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; struct defer_func_ctx ctx;<br />
	&nbsp; &nbsp; memset(&amp;ctx, 0, sizeof(ctx));<br />
	&nbsp; &nbsp; ctx.params_count = arg_count;</span></p>
<p><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; if (arg_count == 0) {<br />
	&nbsp; &nbsp; &nbsp; &nbsp; ctx.ctx.zp.df = fp;</span></p>
<p><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; } else if (arg_count == 1) {<br />
	&nbsp; &nbsp; &nbsp; &nbsp; ctx.ctx.op.df = fp;<br />
	&nbsp; &nbsp; &nbsp; &nbsp; ctx.ctx.op.p1 = va_arg(ap, void*);</span></p>
<p><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; } else if (arg_count == 2) {<br />
	&nbsp; &nbsp; &nbsp; &nbsp; ctx.ctx.tp.df = fp;<br />
	&nbsp; &nbsp; &nbsp; &nbsp; ctx.ctx.tp.p1 = va_arg(ap, void*);<br />
	&nbsp; &nbsp; &nbsp; &nbsp; ctx.ctx.tp.p2 = va_arg(ap, void*);<br />
	&nbsp; &nbsp; &nbsp; &nbsp; ctx.ctx.tp.df(ctx.ctx.tp.p1, ctx.ctx.tp.p2);<br />
	&nbsp; &nbsp; }</span></p>
<p><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; va_end(ap);<br />
	&nbsp; &nbsp; stack_push(&amp;ctx);<br />
	}</span></p>
<p>多个defer的FIFO调用顺序用一个固定大小的stack来实现。这里只是为了演示，所以stack实现的简单和固定些。</p>
<p>组装后的函数在funcexit.c中执行：</p>
<p><span style="font-family:courier new,courier,monospace;">extern struct defer_func_ctx ctx_stack[10];</span></p>
<p><span style="font-family:courier new,courier,monospace;">__attribute__((no_instrument_function))<br />
	void __cyg_profile_func_exit(void *this_fn, void *call_site) {<br />
	&nbsp; &nbsp; struct defer_func_ctx *ctx = NULL;</span></p>
<p><span style="font-family:courier new,courier,monospace;">&nbsp; &nbsp; while ((ctx = stack_pop()) != NULL) {<br />
	&nbsp; &nbsp; &nbsp; &nbsp; if (ctx-&gt;params_count == 0) {<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ctx-&gt;ctx.zp.df();<br />
	&nbsp; &nbsp; &nbsp; &nbsp; } else if (ctx-&gt;params_count == 1) {<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ctx-&gt;ctx.op.df(ctx-&gt;ctx.op.p1);<br />
	&nbsp; &nbsp; &nbsp; &nbsp; } else if (ctx-&gt;params_count == 2) {<br />
	&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ctx-&gt;ctx.tp.df(ctx-&gt;ctx.tp.p1, ctx-&gt;ctx.tp.p2);<br />
	&nbsp; &nbsp; &nbsp; &nbsp; }<br />
	&nbsp; &nbsp; }<br />
	}</span></p>
<p>最后我们将defer.c、funcexit.c编译成一个.so文件：</p>
<p><span style="font-family:courier new,courier,monospace;">gcc -g -fPIC -shared -o libcdefer.so funcexit.c defer.c</span></p>
<p>而编译main.c的方法如下：</p>
<p><span style="font-family:courier new,courier,monospace;">gcc -g main.c -o main -finstrument-functions -I ../lib -L ../lib -lcdefer</span></p>
<p>一切OK后，先将libcdefer.so放在main同级目录下，执行main即可。</p>
<p><span style="font-family:courier new,courier,monospace;">$&gt; ./main<br />
	a = [13], s = [hello]</span></p>
<p>具体代码已经传至<a href="http://code.google.com/p/bigwhite-code">这里</a>(trunk/cdefer)，需要的童鞋可自行下载。&nbsp;</p>
<p style='text-align:left'>&copy; 2013, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2013/02/03/implement-go-defer-in-c/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
	</channel>
</rss>
