<?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>C/C++语言 | 酷 壳 - CoolShell</title>
	<atom:link href="https://coolshell.cn/category/proglanguage/cplusplus/feed" rel="self" type="application/rss+xml" />
	<link>https://coolshell.cn</link>
	<description>享受编程和技术所带来的快乐 - Coding Your Ambition</description>
	<lastBuildDate>Mon, 12 Dec 2022 02:56:31 +0000</lastBuildDate>
	<language>zh-CN</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.2</generator>
	<item>
		<title>eBPF 介绍</title>
		<link>https://coolshell.cn/articles/22320.html</link>
					<comments>https://coolshell.cn/articles/22320.html#comments</comments>
		
		<dc:creator><![CDATA[陈皓]]></dc:creator>
		<pubDate>Sat, 10 Dec 2022 02:38:51 +0000</pubDate>
				<category><![CDATA[C/C++语言]]></category>
		<category><![CDATA[Unix/Linux]]></category>
		<category><![CDATA[操作系统]]></category>
		<category><![CDATA[网络安全]]></category>
		<category><![CDATA[Debug]]></category>
		<category><![CDATA[eBPF]]></category>
		<category><![CDATA[Linux]]></category>
		<guid isPermaLink="false">https://coolshell.cn/?p=22320</guid>

					<description><![CDATA[<p>很早前就想写一篇关于eBPF的文章，但是迟迟没有动手，这两天有点时间，所以就来写一篇，这文章主要还是简单的介绍eBPF 是用来干什么的，并通过几个示例来介绍是怎...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/22320.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/22320.html">eBPF 介绍</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><img decoding="async" loading="lazy" class="alignright wp-image-22329 size-full" src="https://coolshell.cn/wp-content/uploads/2022/12/eBPF.jpeg" alt="" width="301" height="167" />很早前就想写一篇关于eBPF的文章，但是迟迟没有动手，这两天有点时间，所以就来写一篇，这文章主要还是简单的介绍eBPF 是用来干什么的，并通过几个示例来介绍是怎么玩的，这个技术非常非常之强，Linux 操作系统的观测性实在是太强大了，并在 BCC 加持下变得一览无余。这个技术不是一般的运维人员或是系统管理员可以驾驭的，这个还是要有底层系统知识并有一定开发能力的技术人员才能驾驭的了的。<strong>我在这篇文章的最后给了个彩蛋。</strong></p>
<h4>介绍</h4>
<p>eBPF（extened Berkeley Packet Filter）是一种内核技术，它允许开发人员在不修改内核代码的情况下运行特定的功能。eBPF 的概念源自于 Berkeley Packet Filter（BPF），后者是由贝尔实验室开发的一种网络过滤器，可以捕获和过滤网络数据包。</p>
<p>出于对更好的 Linux 跟踪工具的需求，eBPF 从 <a href="https://illumos.org/books/dtrace/chp-intro.html">dtrace</a>中汲取灵感，dtrace 是一种主要用于 Solaris 和 BSD 操作系统的动态跟踪工具。与 dtrace 不同，Linux 无法全面了解正在运行的系统，因为它仅限于系统调用、库调用和函数的特定框架。<a href="https://www.kernel.org/doc/html/latest/bpf/index.html">在Berkeley Packet Filter</a>  (BPF)（一种使用内核 VM 编写打包过滤代码的工具）的基础上，一小群工程师开始扩展 BPF 后端以提供与 dtrace 类似的功能集。 eBPF 诞生了。<strong>2014 年随 Linux 3.18 首次限量发布，充分利用 eBPF 至少需要 Linux 4.4 以上版本</strong>。</p>
<p><span id="more-22320"></span></p>
<p>eBPF 比起传统的 BPF 来说，传统的 BPF 只能用于网络过滤，而 eBPF 则可以用于更多的应用场景，包括网络监控、安全过滤和性能分析等。另外，eBPF 允许常规用户空间应用程序将要在 Linux 内核中执行的逻辑打包为字节码，当某些事件（称为挂钩）发生时，内核会调用 eBPF 程序。此类挂钩的示例包括系统调用、网络事件等。用于编写和调试 eBPF 程序的最流行的工具链称为 <a href="https://github.com/iovisor/bcc">BPF 编译器集合</a> (BCC)，它基于 LLVM 和 CLang。</p>
<p>eBPF 有一些类似的工具。例如，SystemTap 是一种开源工具，可以帮助用户收集 Linux 内核的运行时数据。它通过动态加载内核模块来实现这一功能，类似于 eBPF。另外，DTrace 是一种动态跟踪和分析工具，可以用于收集系统的运行时数据，类似于 eBPF 和 SystemTap。<code>[1]</code></p>
<p>以下是一个简单的比较表格，可以帮助您更好地了解 eBPF、SystemTap 和 DTrace 这三种工具的不同之处：<code>[1]</code></p>
<table>
<thead>
<tr>
<th>工具</th>
<th>eBPF</th>
<th>SystemTap</th>
<th>DTrace</th>
</tr>
</thead>
<tbody>
<tr>
<td>定位</td>
<td>内核技术，可用于多种应用场景</td>
<td>内核模块</td>
<td>动态跟踪和分析工具</td>
</tr>
<tr>
<td>工作原理</td>
<td>动态加载和执行无损编译过的代码</td>
<td>动态加载内核模块</td>
<td>动态插接分析器，通过 probe 获取数据并进行分析</td>
</tr>
<tr>
<td>常见用途</td>
<td>网络监控、安全过滤、性能分析等</td>
<td>系统性能分析、故障诊断等</td>
<td>系统性能分析、故障诊断等</td>
</tr>
<tr>
<td>优点</td>
<td>灵活、安全、可用于多种应用场景</td>
<td>功能强大、可视化界面</td>
<td>功能强大、高性能、支持多种编程语言</td>
</tr>
<tr>
<td>缺点</td>
<td>学习曲线高，安全性依赖于编译器的正确性</td>
<td>学习曲线高，安全性依赖于内核模块的正确性</td>
<td>配置复杂，对系统性能影响较大</td>
</tr>
</tbody>
</table>
<p style="text-align: center;">对比表格<code>[1]</code></p>
<p>从上表可以看出，eBPF、SystemTap 和 DTrace 都是非常强大的工具，可以用于收集和分析系统的运行情况。<code>[1]</code></p>
<h4>用途</h4>
<p>eBPF 是一种非常灵活和强大的内核技术，可以用于多种应用场景。下面是 eBPF 的一些常见用途：<code>[1]</code></p>
<ul>
<li>网络监控：eBPF 可以用于捕获网络数据包，并执行特定的逻辑来分析网络流量。例如，可以使用 eBPF 程序来监控网络流量，并在发现异常流量时进行警报。<code>[1]</code></li>
<li>安全过滤：eBPF 可以用于对网络数据包进行安全过滤。例如，可以使用 eBPF 程序来阻止恶意流量的传播，或者在发现恶意流量时对其进行拦截。<code>[1]</code></li>
<li>性能分析：eBPF 可以用于对内核的性能进行分析。例如，可以使用 eBPF 程序来收集内核的性能指标，并通过特定的接口将其可视化。这样，可以更好地了解内核的性能瓶颈，并进行优化。<code>[1]</code></li>
<li>虚拟化：eBPF 可以用于虚拟化技术。例如，可以使用 eBPF 程序来收集虚拟机的性能指标，并进行负载均衡。这样，可以更好地利用虚拟化环境的资源，提高系统的性能和稳定性。<code>[1]</code></li>
</ul>
<p>总之，eBPF 的常见用途非常广泛，可以用于网络监控、安全过滤、性能分析和虚拟化等多种应用场景。<code>[1]</code></p>
<h4>工作原理</h4>
<p>eBPF 的工作原理主要分为三个步骤：加载、编译和执行。</p>
<p>eBPF 需要在内核中运行。这通常是由用户态的应用程序完成的，它会通过系统调用来加载 eBPF 程序。在加载过程中，内核会将 eBPF 程序的代码复制到内核空间。</p>
<p>eBPF 程序需要经过编译和执行。这通常是由Clang/LLVM的编译器完成，然后形成字节码后，将用户态的字节码装载进内核，Verifier会对要注入内核的程序进行一些内核安全机制的检查,这是为了确保 eBPF 程序不会破坏内核的稳定性和安全性。在检查过程中，内核会对 eBPF 程序的代码进行分析，以确保它不会进行恶意操作，如系统调用、内存访问等。如果 eBPF 程序通过了内核安全机制的检查，它就可以在内核中正常运行了，其会通过通过一个JIT编译步骤将程序的通用字节码转换为机器特定指令集，以优化程序的执行速度。</p>
<p>下图是其架构图。</p>
<p><img decoding="async" class="aligncenter" src="https://imgopt.infoq.com/fit-in/1200x2400/filters:quality(80)/filters:no_upscale()/articles/gentle-linux-ebpf-introduction/en/resources/47image005-1619704397592.jpg" /></p>
<p style="text-align: center;">（图片来自：<a href="https://www.infoq.com/articles/gentle-linux-ebpf-introduction/" target="_blank" rel="noopener">https://www.infoq.com/articles/gentle-linux-ebpf-introduction/</a>）</p>
<p>在内核中运行时，eBPF 程序通常会挂载到一个内核钩子（hook）上，以便在特定的事件发生时被执行。例如，</p>
<ul>
<li><span>系统调用——当用户空间函数将执行转移到内核时插入</span></li>
<li><span>函数进入和退出——拦截对预先存在的函数的调用</span></li>
<li><span>网络事件 &#8211; 在收到数据包时执行</span></li>
<li><span>Kprobes 和 uprobes &#8211; 附加到内核或用户函数的探测器</span></li>
</ul>
<p>最后 eBPF Maps，允许eBPF程序在调用之间保持状态，以便进行相关的数据统计，并与用户空间的应用程序共享数据。一个eBPF映射基本上是一个键值存储，其中的值通常被视为任意数据的二进制块。它们是通过带有BPF_MAP_CREATE参数的<code>bpf_cmd</code>系统调用来创建的，和Linux世界中的其他东西一样，它们是通过文件描述符来寻址。与地图的交互是通过查找/更新/删除系统调用进行的</p>
<p>总之，eBPF 的工作原理是通过动态加载、执行和检查<strong>无损编译</strong>过的代码来实现的。<code>[1]</code></p>
<h4>示例</h4>
<p>eBPF 可以用于对内核的性能进行分析。下面是一个基于 eBPF 的性能分析的 step-by-step 示例：</p>
<p>第一步：准备工作：首先，需要确保内核已经支持 eBPF 功能。这通常需要在内核配置文件中启用 eBPF 相关的选项，并重新编译内核。检查是否支持 eBPF，你可以用这两个命令查看 <code>ls /sys/fs/bpf</code> 和 <code>lsmod | grep bpf</code></p>
<p>第二步：写 eBPF 程序：接下来，需要编写 eBPF 程序，用于收集内核的性能指标。eBPF 程序的语言可以选择 C 或者 Python，它需要通过特定的接口访问内核的数据结构，并将收集到的数据保存到指定的位置。</p>
<p>下面是一个Python 示例（其实还是C语言，用python来加载一段C程序到Linux内核）</p>
<pre class="EnlighterJSRAW" data-enlighter-language="python">#!/usr/bin/python3

from bcc import BPF
from time import sleep

# 定义 eBPF 程序
bpf_text = """
#include &lt;uapi/linux/ptrace.h&gt;

BPF_HASH(stats, u32);

int count(struct pt_regs *ctx) {
    u32 key = 0;
    u64 *val, zero=0;
    val = stats.lookup_or_init(&amp;key, &amp;zero);
    (*val)++;
    return 0;
}
"""

# 编译 eBPF 程序
b = BPF(text=bpf_text, cflags=["-Wno-macro-redefined"])

# 加载 eBPF 程序
b.attach_kprobe(event="tcp_sendmsg", fn_name="count")

name = {
  0: "tcp_sendmsg"
}
# 输出统计结果
while True:
    try:
        #print("Total packets: %d" % b["stats"][0].value)
        for k, v in b["stats"].items():
           print("{}: {}".format(name[k.value], v.value))
        sleep(1)
    except KeyboardInterrupt:
        exit()</pre>
<p>这个 eBPF 程序的功能是统计网络中传输的数据包数量。它通过定义一个 <code>BPF_HASH</code> 数据结构来保存统计结果（eBPF Maps），并通过捕获 <code>tcp_sendmsg</code> 事件来实现实时统计。最后，它通过每秒输出一次统计结果来展示数据。这个 eBPF 程序只是一个简单的示例，实际应用中可能需要进行更复杂的统计和分析。</p>
<p>第三步：运行 eBPF 程序：接下来，需要使用 eBPF 编译器将 eBPF 程序编译成内核可执行的格式（这个在上面的Python程序里你可以看到——Python引入了一个bcc的包，然后用这个包，把那段 C语言的程序编译成字节码加载在内核中并把某个函数 attach 到某个事件上）。这个过程可以使用 BPF Compiler Collection（BCC）工具来完成。BCC 工具可以通过命令行的方式将 eBPF 程序编译成内核可执行的格式，并将其加载到内核中。</p>
<p>下面是运行上面的 Python3 程序的步骤：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="shell">sudo apt install python3-bpfcc</pre>
<p>注：在Python3下请不要使用 <code>pip3 install bcc</code> （参看：<a href="https://github.com/iovisor/bcc/issues/2278#issuecomment-825356087" target="_blank" rel="noopener">这里</a>）</p>
<p>如果你是 Ubuntu 20.10 以上的版本，最好通过源码安装（否则程序会有编译问题），参看：<a href="https://github.com/iovisor/bcc/issues/3993#issuecomment-1228217609" target="_blank" rel="noopener">这里</a>：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="bash">apt purge bpfcc-tools libbpfcc python3-bpfcc
wget https://github.com/iovisor/bcc/releases/download/v0.25.0/bcc-src-with-submodule.tar.gz
tar xf bcc-src-with-submodule.tar.gz
cd bcc/
apt install -y python-is-python3
apt install -y bison build-essential cmake flex git libedit-dev   libllvm11 llvm-11-dev libclang-11-dev zlib1g-dev libelf-dev libfl-dev python3-distutils
apt install -y checkinstall
mkdir build
cd build/
cmake -DCMAKE_INSTALL_PREFIX=/usr -DPYTHON_CMD=python3 ..
make
checkinstall</pre>
<p>接下来，需要将上面的 Python 程序保存到本地，例如保存到文件 netstat.py。运行程序：最后，可以通过执行以下命令来运行 Python 程序：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="shell">$ chmod +x ./netstat.py
$ sudo ./netstat.py
tcp_sendmsg: 29
tcp_sendmsg: 216
tcp_sendmsg: 277
tcp_sendmsg: 379
tcp_sendmsg: 419
tcp_sendmsg: 468
tcp_sendmsg: 574
tcp_sendmsg: 645
tcp_sendmsg: 29
</pre>
<p>程序开始运行后，会在控制台输出网络数据包的统计信息。可以通过按 Ctrl+C 组合键来结束程序的运行。</p>
<p>下面我们再看一个比较复杂的示例，这个示例会计算TCP的发包时间（示例参考于Github上 <a href="https://github.com/iovisor/bcc/issues/2972" target="_blank" rel="noopener">这个issue</a>里的程序）：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="python">#!/usr/bin/python3

from bcc import BPF
import time

# 定义 eBPF 程序
bpf_text = """
#include &lt;uapi/linux/ptrace.h&gt;
#include &lt;net/sock.h&gt;
#include &lt;net/inet_sock.h&gt;
#include &lt;bcc/proto.h&gt;

struct packet_t {
    u64 ts, size;
    u32 pid;
    u32 saddr, daddr;
    u16 sport, dport;
};

BPF_HASH(packets, u64, struct packet_t);

int on_send(struct pt_regs *ctx, struct sock *sk, struct msghdr *msg, size_t size)
{
    u64 id = bpf_get_current_pid_tgid();
    u32 pid = id;

    // 记录数据包的时间戳和信息
    struct packet_t pkt = {}; // 结构体一定要初始化，可以使用下面的方法
                              //__builtin_memset(&amp;pkt, 0, sizeof(pkt)); 
    pkt.ts = bpf_ktime_get_ns();
    pkt.size = size;
    pkt.pid = pid;
    pkt.saddr = sk-&gt;__sk_common.skc_rcv_saddr;
    pkt.daddr = sk-&gt;__sk_common.skc_daddr;
    struct inet_sock *sockp = (struct inet_sock *)sk;
    pkt.sport = sockp-&gt;inet_sport;
    pkt.dport = sk-&gt;__sk_common.skc_dport;

    packets.update(&amp;id, &amp;pkt);
    return 0;
}

int on_recv(struct pt_regs *ctx, struct sock *sk)
{
    u64 id = bpf_get_current_pid_tgid();
    u32 pid = id;

    // 获取数据包的时间戳和编号
    struct packet_t *pkt = packets.lookup(&amp;id);
    if (!pkt) {
        return 0;
    }

    // 计算传输时间
    u64 delta = bpf_ktime_get_ns() - pkt-&gt;ts;

    // 统计结果
    bpf_trace_printk("tcp_time: %llu.%llums, size: %llu\\n", 
       delta/1000, delta%1000%100, pkt-&gt;size);

    // 删除统计结果
    packets.delete(&amp;id);

    return 0;
}
"""

# 编译 eBPF 程序
b = BPF(text=bpf_text, cflags=["-Wno-macro-redefined"])

# 注册 eBPF 程序
b.attach_kprobe(event="tcp_sendmsg", fn_name="on_send")
b.attach_kprobe(event="tcp_v4_do_rcv", fn_name="on_recv")

# 输出统计信息
print("Tracing TCP latency... Hit Ctrl-C to end.")
while True:
    try:
        (task, pid, cpu, flags, ts, msg) = b.trace_fields()
        print("%-18.9f %-16s %-6d %s" % (ts, task, pid, msg))
    except KeyboardInterrupt:
        exit()</pre>
<p>上面这个程序通过捕获每个数据包的时间戳来统计传输时间。在捕获 <code>tcp_sendmsg</code> 事件时，记录数据包的发送时间；在捕获 <code>tcp_v4_do_rcv</code> 事件时，记录数据包的接收时间；最后，通过比较两个时间戳来计算传输时间。</p>
<p>从上面的两个程序我们可以看到，eBPF 的一个编程的基本方法，这样的在Python里向内核的某些事件挂载一段 &#8220;C语言” 的方式就是 eBPF 的编程方式。实话实说，这样的代码很不好写，而且有很多非常诡异的东西，一般人是很难驾驭的（上面的代码我也不是很容易都能写通的，把 Google 都用了个底儿掉，读了很多晦涩的文档……）好在这样的代码已经有人写了，我们不必再写了，在 <a href="https://github.com/iovisor/bcc/tree/master/tools" target="_blank" rel="noopener">Github 上的 bcc 库下的 tools 目录</a>有很多……</p>
<p>BCC（<a href="https://github.com/iovisor/bcc" target="_blank" rel="noopener">BPF Compiler Collection</a>）是一套开源的工具集，可以在 Linux 系统中使用 BPF（Berkeley Packet Filter）程序进行系统级性能分析和监测。BCC 包含了许多实用工具，如：</p>
<ol>
<li>bcc-tools：一个包含许多常用的 BCC 工具的软件包。</li>
<li>bpftrace：一个高级语言，用于编写和执行 BPF 程序。</li>
<li>tcptop：一个实时监控和分析 TCP 流量的工具。</li>
<li>execsnoop：一个用于监控进程执行情况的工具。</li>
<li>filetop：一个实时监控和分析文件系统流量的工具。</li>
<li>trace：一个用于跟踪和分析函数调用的工具。</li>
<li>funccount：一个用于统计函数调用次数的工具。</li>
<li>opensnoop：一个用于监控文件打开操作的工具。</li>
<li>pidstat：一个用于监控进程性能的工具。</li>
<li>profile：一个用于分析系统 CPU 使用情况的工具。</li>
</ol>
<p>下面这张图你可能见过多次了，你可以看看他可以干多少事，内核里发生什么事一览无余。</p>
<p><img decoding="async" src="https://github.com/iovisor/bcc/raw/master/images/bcc_tracing_tools_2019.png" /></p>
<h4>延伸阅读</h4>
<p>一些经典的文章和书籍关于 eBPF 包括：</p>
<ul>
<li>Brendan Gregg 的《<a href="https://book.douban.com/subject/34467459/" target="_blank" rel="noopener">BPF Performance Tools: Linux System and Application Observability</a>》一书是一个全面的指南，涵盖了 eBPF 的基础知识和实践应用。</li>
<li>eBPF 的官网：<a href="https://ebpf.io/" target="_blank" rel="noopener">https://ebpf.io/</a> 由 <a href="https://cilium.io/" rel="nofollow">Cilium</a> 建立</li>
<li><a href="http://docs.cilium.io/en/latest/bpf/" rel="nofollow">Cilium&#8217;s BPF and XDP Reference Guide</a></li>
<li><a href="https://www.kernel.org/doc/html/latest/bpf/index.html" rel="nofollow">BPF Documentation</a></li>
<li><a href="https://www.kernel.org/doc/html/latest/bpf/bpf_design_QA.html" rel="nofollow">BPF Design Q&amp;A</a></li>
<li>还有 Github 上的 <a href="https://github.com/zoidbergwill/awesome-ebpf" target="_blank" rel="noopener">Awesome eBPF</a></li>
</ul>
<h4>彩蛋</h4>
<p>最后来到彩蛋环节。因为最近 ChatGPT 很火，于是，我想通过 ChatGPT 来帮助我书写这篇文章，一开始我让ChatGPT 帮我列提纲，并根据提纲生成文章内容，并查找相关的资料，非常之顺利，包括生成的代码，我以为我们以很快地完成这篇文章。</p>
<p>但是，到了代码生成的时候，我发现，ChatGPT 生成的代码的思路和方法都是对的，但是是比较老的，而且是跑不起来的，<strong>出现了好些低级错误，如：使用了未声明的变量，没有引用完整的C语言的头文件，没有正确地初始化变量，错误地获取数据，类型没有匹配……等等</strong>，在程序调试上，挖了很多的坑，C语言本来就不好搞，挖的很多运行时的坑很难察觉，所以，耗费了我大量的时间来排除各种各样的问题，其中有环境上的问题，还有代码上的问题，这些问题即便是通过 Google 也不容易找到解决方案，我找到的解决方案都放在文章中了，尤其是第二个示例，让我调试了3个多小时，读了很多 bcc 上的issue和相关的晦涩的手册和文档，才让程序跑通。</p>
<p>到了文章收关的阶段，我让ChatGPT 给我几个延伸阅读，也是很好的，但是没有给出链接，于是我只得人肉 Google 了一下，然后让我吃惊的是，<strong>好多ChatGPT给出来的文章是根本不存在的，完全是它伪造的</strong>。我连让它干了两次都是这样，这个让我惊掉大牙。这让我开始怀疑它之前生成的内容，于是，我不得我返回仔细Review我的文章，尤其是“介绍”、“用途”和“工作原理”这三个章节，基本都是ChatGPT生成的，在Review完后，我发现了ChatGPT 给我生造了一个叫 “无损编译器”的术语，这个术语简直了，于是我开始重写我的文章。我把一些段落重写了，有一些没有，保留下来的我都标记上了 <code>[1]</code>，大家读的时候要小心阅读。</p>
<p>最后，<strong>我的结论是，ChatGPT只是一个不成熟的玩具，只能回答一些没有价值的日常聊天的问题，要说能取代Google，我觉得不可能，因为Google会基于基本的事实，而ChatGPT会基于内容生成的算法，在造假方面称得上是高手，可以列为电信诈骗的范畴了，我以后不会再使用ChatGPT生成文章内容或是作我的帮手了。StackOverflow把其ban了真是不能太赞了！</strong></p>
<p><strong>附件一：ChatGPT的造假载图和样本</strong></p>
<table width="100%">
<tbody>
<tr>
<td>
<p><figure id="attachment_22325" aria-describedby="caption-attachment-22325" style="width: 195px" class="wp-caption aligncenter"><a href="https://coolshell.cn/wp-content/uploads/2022/12/eBPF.ref_.1.png"><img decoding="async" loading="lazy" class="wp-image-22325 size-medium" src="https://coolshell.cn/wp-content/uploads/2022/12/eBPF.ref_.1-195x300.png" alt="" width="195" height="300" srcset="https://coolshell.cn/wp-content/uploads/2022/12/eBPF.ref_.1-195x300.png 195w, https://coolshell.cn/wp-content/uploads/2022/12/eBPF.ref_.1-665x1024.png 665w, https://coolshell.cn/wp-content/uploads/2022/12/eBPF.ref_.1-768x1183.png 768w, https://coolshell.cn/wp-content/uploads/2022/12/eBPF.ref_.1-997x1536.png 997w, https://coolshell.cn/wp-content/uploads/2022/12/eBPF.ref_.1-175x270.png 175w, https://coolshell.cn/wp-content/uploads/2022/12/eBPF.ref_.1.png 1096w" sizes="(max-width: 195px) 100vw, 195px" /></a><figcaption id="caption-attachment-22325" class="wp-caption-text">点击看大图</figcaption></figure></td>
<td>
<p><figure id="attachment_22326" aria-describedby="caption-attachment-22326" style="width: 286px" class="wp-caption aligncenter"><a href="https://coolshell.cn/wp-content/uploads/2022/12/eBPF.ref_.2.png"><img decoding="async" loading="lazy" class="wp-image-22326 size-medium" src="https://coolshell.cn/wp-content/uploads/2022/12/eBPF.ref_.2-286x300.png" alt="" width="286" height="300" srcset="https://coolshell.cn/wp-content/uploads/2022/12/eBPF.ref_.2-286x300.png 286w, https://coolshell.cn/wp-content/uploads/2022/12/eBPF.ref_.2-978x1024.png 978w, https://coolshell.cn/wp-content/uploads/2022/12/eBPF.ref_.2-768x804.png 768w, https://coolshell.cn/wp-content/uploads/2022/12/eBPF.ref_.2-258x270.png 258w, https://coolshell.cn/wp-content/uploads/2022/12/eBPF.ref_.2.png 1096w" sizes="(max-width: 286px) 100vw, 286px" /></a><figcaption id="caption-attachment-22326" class="wp-caption-text">点击看大图</figcaption></figure></td>
</tr>
</tbody>
</table>
<p><strong>ChatGPT 生成的样本一</strong></p>
<ul>
<li>Brendan Gregg 的《BPF Performance Tools: Linux System and Application Observability》一书是一个全面的指南，涵盖了 eBPF 的基础知识和实践应用。</li>
<li>Alexei Starovoitov 在 2015 年的 LPC 大会上发表的《eBPF and XDP: fast programmable datapath in the Linux kernel》一文，是一个介绍 eBPF 和 XDP（eXpress Data Path）的综述。</li>
<li>谷歌的《BPF: The Future of Linux Monitoring and Observability》一文介绍了 eBPF 的基本概念，并探讨了它对于 Linux 监控和可观察性的影响。</li>
<li>微软的《A Survey of Techniques for Hardware Acceleration of BPF》一文，介绍了 eBPF 在硬件加速方面的研究现状。</li>
<li>Daniel Borkmann 的《Linux kernel networking: implementation and theory》一书涵盖了 eBPF 的实现和理论。</li>
<li>Martin KaFai Lau 在 2015 年的 SOSP 大会上发表的《eBPF: a new execution environment for the Linux kernel》一文，介绍了 eBPF 在 Linux 内核中的新执行环境。</li>
<li>来自两名谷歌工程师的《eBPF: A New Frontier in Performance Analysis》一文，探讨了 eBPF 对性能分析的影响。</li>
<li>《BPF Performance Tools: Linux System and Application Observability》一书中的第三章，讨论了 eBPF 的编译器和运行时系统。</li>
<li>来自三名谷歌工程师的《eBPF: The New Solaris DTrace》一文，比较了 eBPF 和 Solaris DTrace 系统的区别。</li>
</ul>
<p><strong>ChatGPT 生成的样本二</strong></p>
<ol>
<li>eBPF 程序开发指南（<a href="https://cilium.io/doc/ebpf/ebpf-guide/" target="_new" rel="noopener">https://cilium.io/doc/ebpf/ebpf-guide/</a>）</li>
<li>eBPF 官方文档（<a href="https://www.kernel.org/doc/Documentation/networking/filter.txt" target="_new" rel="noopener">https://www.kernel.org/doc/Documentation/networking/filter.txt</a>）</li>
<li>Linux 内核 eBPF 介绍（<a href="https://www.ibm.com/developerworks/cn/linux/kernel/ebpf/index.html" target="_new" rel="noopener">https://www.ibm.com/developerworks/cn/linux/kernel/ebpf/index.html</a>）</li>
<li>eBPF 性能优化技巧（<a href="https://www.infoq.com/cn/news/2018/08/ebpf-performance-tips/" target="_new" rel="noopener">https://www.infoq.com/cn/news/2018/08/ebpf-performance-tips/</a>）</li>
<li>eBPF 实战经验分享（<a href="https://blog.cloudflare.com/ebpf-in-practice/" target="_new" rel="noopener">https://blog.cloudflare.com/ebpf-in-practice/</a>）</li>
<li>eBPF 在网络安全中的应用（<a href="https://www.zdnet.com/article/ebpf-how-to-use-it-for-network-security/" target="_new" rel="noopener">https://www.zdnet.com/article/ebpf-how-to-use-it-for-network-security/</a>）</li>
<li>eBPF 实现 Linux 系统性能监控（<a href="https://www.percona.com/blog/2017/08/15/how-to-use-ebpf-to-monitor-linux-system-performance/" target="_new" rel="noopener">https://www.percona.com/blog/2017/08/15/how-to-use-ebpf-to-monitor-linux-system-performance/</a>）</li>
<li>eBPF 入门教程（<a href="https://sysdig.com/blog/ebpf-getting-started/" target="_new" rel="noopener">https://sysdig.com/blog/ebpf-getting-started/</a>）</li>
<li>eBPF 与 BPF 比较（<a href="https://lwn.net/Articles/724647/" target="_new" rel="noopener">https://lwn.net/Articles/724647/</a>）</li>
<li>eBPF 提高课程（<a href="https://www.pluralsight.com/courses/ebpf-advanced" target="_new" rel="noopener">https://www.pluralsight.com/courses/ebpf-advanced</a>）</li>
</ol>
<p><strong>附件二：发明的术语：无损编译器</strong></p>
<table width="100%">
<tbody>
<tr>
<td>
<p><figure id="attachment_22328" aria-describedby="caption-attachment-22328" style="width: 180px" class="wp-caption aligncenter"><a href="https://coolshell.cn/wp-content/uploads/2022/12/fake.png"><img decoding="async" loading="lazy" class="wp-image-22328 size-medium" src="https://coolshell.cn/wp-content/uploads/2022/12/fake-180x300.png" alt="" width="180" height="300" srcset="https://coolshell.cn/wp-content/uploads/2022/12/fake-180x300.png 180w, https://coolshell.cn/wp-content/uploads/2022/12/fake-616x1024.png 616w, https://coolshell.cn/wp-content/uploads/2022/12/fake-768x1277.png 768w, https://coolshell.cn/wp-content/uploads/2022/12/fake-924x1536.png 924w, https://coolshell.cn/wp-content/uploads/2022/12/fake-162x270.png 162w, https://coolshell.cn/wp-content/uploads/2022/12/fake.png 1080w" sizes="(max-width: 180px) 100vw, 180px" /></a><figcaption id="caption-attachment-22328" class="wp-caption-text">点击看大图</figcaption></figure></td>
<td>
<p><figure id="attachment_22335" aria-describedby="caption-attachment-22335" style="width: 223px" class="wp-caption aligncenter"><a href="https://coolshell.cn/wp-content/uploads/2022/12/fake.term_.png"><img decoding="async" loading="lazy" class="wp-image-22335 size-medium" src="https://coolshell.cn/wp-content/uploads/2022/12/fake.term_-223x300.png" alt="" width="223" height="300" srcset="https://coolshell.cn/wp-content/uploads/2022/12/fake.term_-223x300.png 223w, https://coolshell.cn/wp-content/uploads/2022/12/fake.term_-761x1024.png 761w, https://coolshell.cn/wp-content/uploads/2022/12/fake.term_-768x1033.png 768w, https://coolshell.cn/wp-content/uploads/2022/12/fake.term_-1142x1536.png 1142w, https://coolshell.cn/wp-content/uploads/2022/12/fake.term_-201x270.png 201w, https://coolshell.cn/wp-content/uploads/2022/12/fake.term_.png 1176w" sizes="(max-width: 223px) 100vw, 223px" /></a><figcaption id="caption-attachment-22335" class="wp-caption-text">点击看大图</figcaption></figure></td>
</tr>
</tbody>
</table>
<p>（全文完）<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" id="wp_rp_first"><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/1379.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/13.jpg" alt="如何调试bash脚本" width="150" height="150" /></a><a href="https://coolshell.cn/articles/1379.html" class="wp_rp_title">如何调试bash脚本</a></li><li ><a href="https://coolshell.cn/articles/19219.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2019/03/linux.ninja_-150x150.png" alt="打造高效的工作环境 &#8211; Shell 篇" width="150" height="150" /></a><a href="https://coolshell.cn/articles/19219.html" class="wp_rp_title">打造高效的工作环境 &#8211; Shell 篇</a></li><li ><a href="https://coolshell.cn/articles/18654.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2018/12/docker-networking-1-150x150.png" alt="记一次Kubernetes/Docker网络排障" width="150" height="150" /></a><a href="https://coolshell.cn/articles/18654.html" class="wp_rp_title">记一次Kubernetes/Docker网络排障</a></li><li ><a href="https://coolshell.cn/articles/18360.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2018/05/300x262-150x150.jpg" alt="程序员练级攻略（2018)  与我的专栏" width="150" height="150" /></a><a href="https://coolshell.cn/articles/18360.html" class="wp_rp_title">程序员练级攻略（2018)  与我的专栏</a></li><li ><a href="https://coolshell.cn/articles/17998.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2017/07/systemd-1-150x150.jpeg" alt="Linux PID 1 和 Systemd" width="150" height="150" /></a><a href="https://coolshell.cn/articles/17998.html" class="wp_rp_title">Linux PID 1 和 Systemd</a></li><li ><a href="https://coolshell.cn/articles/17416.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2016/07/cache-150x150.png" alt="缓存更新的套路" width="150" height="150" /></a><a href="https://coolshell.cn/articles/17416.html" class="wp_rp_title">缓存更新的套路</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/22320.html">eBPF 介绍</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/22320.html/feed</wfw:commentRss>
			<slash:comments>18</slash:comments>
		
		
			</item>
		<item>
		<title>API设计原则 &#8211; Qt官网的设计实践总结</title>
		<link>https://coolshell.cn/articles/18024.html</link>
					<comments>https://coolshell.cn/articles/18024.html#comments</comments>
		
		<dc:creator><![CDATA[李 鼎]]></dc:creator>
		<pubDate>Tue, 25 Jul 2017 06:16:30 +0000</pubDate>
				<category><![CDATA[C/C++语言]]></category>
		<category><![CDATA[技术读物]]></category>
		<category><![CDATA[程序设计]]></category>
		<category><![CDATA[系统架构]]></category>
		<category><![CDATA[API]]></category>
		<category><![CDATA[api-design]]></category>
		<category><![CDATA[API设计]]></category>
		<category><![CDATA[C++]]></category>
		<category><![CDATA[Coding]]></category>
		<category><![CDATA[Design]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[qt]]></category>
		<category><![CDATA[程序员]]></category>
		<guid isPermaLink="false">http://coolshell.cn/?p=18024</guid>

					<description><![CDATA[<p>（感谢好友 @李鼎 翻译此文） 原文链接：API Design Principles &#8211; Qt Wiki 基于Gary的影响力上 Gary Gao ...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/18024.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/18024.html">API设计原则 – Qt官网的设计实践总结</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><span style="color: #993300;"><strong>（感谢好友 <a href="http://www.weibo.com/oldratlee" target="_blank"  rel="noopener noreferrer">@李鼎</a> 翻译此文）</strong></span></p>
<p>原文链接：<a href="http://qt-project.org/wiki/API-Design-Principles">API Design Principles</a> &#8211; <a href="http://wiki.qt.io/">Qt Wiki</a><br />
基于<a href="http://blog.csdn.net/gaoyingju">Gary的影响力</a>上 <em>Gary Gao</em> 的译文稿：<a href="http://blog.csdn.net/gaoyingju/article/details/8245108">C++的API设计指导</a></p>
<h2>译序</h2>
<p><img decoding="async" loading="lazy" class="alignright size-medium" src="https://coolshell.cn/wp-content/uploads/2017/07/api-design-300x278.jpg" alt="" width="300" height="278" /></p>
<p>Qt的设计水准在业界很有口碑，一致、易于掌握和强大的API是Qt最著名的优点之一。此文既是Qt官网上的API设计指导准则，也是Qt在API设计上的实践总结。虽然Qt用的是C++，但其中设计原则和思考是具有普适性的（如果你对C++还不精通，可以忽略与C++强相关或是过于细节的部分，仍然可以学习或梳理关于API设计最有价值的内容）。整个篇幅中有很多示例，是关于API设计一篇难得的好文章。</p>
<p>需要注意的是，这篇Wiki有一些内容并不完整，所以，可能会有一些阅读上的问题，我们对此做了一些相关的注释。</p>
<p>PS：翻译中肯定会有不足和不对之处，欢迎评论&amp;交流；另译文源码在<a href="https://github.com/oldratlee/translations/tree/master/api-design-principles-from-qt">GitHub的这个仓库</a>中，可以<a href="https://github.com/oldratlee/translations/issues">提交Issue</a>/<a href="https://github.com/oldratlee/translations/fork">Fork后提交代码</a>来建议/指正。</p>
<h1>API设计原则</h1>
<p>一致、易于掌握和强大的API是Qt最著名的优点之一。此文总结了我们在设计Qt风格API的过程中所积累的诀窍（know-how）。其中许多是通用准则；而其他的则更偏向于约定，遵循这些约定主要是为了与已有的API保持一致。</p>
<p>虽然这些准则主要用于对外的API（public API），但在设计对内的API（private API）时也推荐遵循相同的技巧（techniques），作为开发者之间协作的礼仪（courtesy）。</p>
<p><span id="more-18024"></span></p>
<p>如有兴趣也可以读一下 <em>Jasmin Blanchette</em> 的<a href="http://www4.in.tum.de/~blanchet/api-design.pdf">Little Manual of API Design (PDF)</a> 或是本文的前身 <em>Matthias Ettrich</em> 的<a href="https://doc.qt.io/archives/qq/qq13-apis.html">Designing Qt-Style C++ APIs</a>。</p>
<h1>1. 好API的6个特质</h1>
<p>API之于程序员就如同图形界面之于普通用户（end-user）。API中的『P』实际上指的是『程序员』（Programmer），而不是『程序』（Program），强调的是API是给程序员使用的这一事实。</p>
<p>在第13期<a href="http://doc.qt.io/archives/qq/">Qt季刊</a>，<em>Matthias</em> 的<a href="https://doc.qt.io/archives/qq/qq13-apis.html">关于API设计的文章</a>中提出了观点：API应该极简（minimal）且完备（complete）、语义清晰简单（have clear and simple semantics）、符合直觉（be intuitive）、易于记忆（be easy to memorize）和引导API使用者写出可读代码（lead to readable code）。</p>
<h2>1.1 极简</h2>
<p>极简的API是指每个class的public成员尽可能少，public的class也尽可能少。这样的API更易理解、记忆、调试和变更。</p>
<h2>1.2 完备</h2>
<p>完备的API是指期望有的功能都包含了。这点会和保持API极简有些冲突。如果一个成员函数放在错误的类中，那么这个函数的潜在用户就会找不到，这也是违反完备性的。</p>
<h2>1.3 语义清晰简单</h2>
<p>就像其他的设计一样，我们应该遵守最少意外原则（the principle of least surprise）。好的API应该可以让常见的事完成的更简单，并有可以完成不常见的事的可能性，但是却不会关注于那些不常见的事。解决的是具体问题；当没有需求时不要过度通用化解决方案。（举个例子，在Qt 3中，<code>QMimeSourceFactory</code>不应命名成<code>QImageLoader</code>并有不一样的API。）</p>
<h2>1.4 符合直觉</h2>
<p>就像计算机里的其他事物一样，API应该符合直觉。对于什么是符合直觉的什么不符合，不同经验和背景的人会有不同的看法。API符合直觉的测试方法：经验不很丰富的用户不用阅读API文档就能搞懂API，而且程序员不用了解API就能看明白使用API的代码。</p>
<h2>1.5 易于记忆</h2>
<p>为使API易于记忆，API的命名约定应该具有一致性和精确性。使用易于识别的模式和概念，并且避免用缩写。</p>
<h2>1.6 引导API使用者写出可读代码</h2>
<p>代码只写一次，却要多次的阅读（还有调试和修改）。写出可读性好的代码有时候要花费更多的时间，但对于产品的整个生命周期来说是节省了时间的。</p>
<p>最后，要记住的是，不同的用户会使用API的不同部分。尽管简单使用单个Qt类的实例应该符合直觉，但如果是要继承一个类，让用户事先看好文档是个合理的要求。</p>
<h1>2. 静态多态</h1>
<p>相似的类应该有相似的API。在继承（inheritance）合适时可以用继承达到这个效果，即运行时多态。然而多态也发生在设计阶段。例如，如果你用<code>QProgressBar</code>替换<code>QSlider</code>，或是用<code>QString</code>替换<code>QByteArray</code>，你会发现API的相似性使的替换很容易。这即是所谓的『静态多态』（static polymorphism）。</p>
<p>静态多态也使记忆API和编程模式更加容易。因此，一组相关的类有相似的API有时候比每个类都有各自的一套API更好。</p>
<p>一般来说，在Qt中，如果没有足够的理由要使用继承，我们更倾向于用静态多态。这样可以减少Qt public类的个数，也使刚学习Qt的用户在翻看文档时更有方向感。</p>
<h2>2.1 好的案例</h2>
<p><code>QDialogButtonBox</code>与<code>QMessageBox</code>，在处理按钮（<code>addButton()</code>、<code>setStandardButtons()</code>等等）上有相似的API，不需要继承某个<code>QAbstractButtonBox</code>类。</p>
<h2>2.2 差的案例</h2>
<p><code>QTcpSocket</code>与<code>QUdpSocket</code>都继承了<code>QAbstractSocket</code>，这两个类的交互行为的模式（mode of interaction）非常不同。似乎没有什么人以通用和有意义的方式用过<code>QAbstractSocket</code>指针（或者 <strong><em>能</em></strong> 以通用和有意义的方式使用<code>QAbstractSocket</code>指针）。</p>
<h2>2.3 值得斟酌的案例</h2>
<p><code>QBoxLayout</code>是<code>QHBoxLayout</code>与<code>QVBoxLayout</code>的父类。好处：可以在工具栏上使用<code>QBoxLayout</code>，调用<code>setOrientation()</code>使其变为水平/垂直。坏处：要多一个类，并且有可能导致用户写出这样没什么意义的代码，<code>((QBoxLayout *)hbox)-&gt;setOrientation(Qt::Vertical)</code>。</p>
<h1>3. 基于属性的API</h1>
<p>新的Qt类倾向于用『基于属性（property）的API』，例如：</p>
<p>[code language=&#8221;cpp&#8221;]<br />
QTimer timer;<br />
timer.setInterval(1000);<br />
timer.setSingleShot(true);<br />
timer.start();<br />
[/code]</p>
<p>这里的 <strong><em>属性</em></strong> 是指任何的概念特征（conceptual attribute），是对象状态的一部分 —— 无论它是不是<code>Q_PROPERTY</code>。在说得通的情况下，用户应该可以以任何顺序设置属性，也就是说，属性之间应该是正交的（orthogonal）。例如，上面的代码可以写成：</p>
<p>[code language=&#8221;cpp&#8221;]<br />
QTimer timer;<br />
timer.setSingleShot(true);<br />
timer.setInterval(1000);<br />
timer.start();<br />
[/code]</p>
<blockquote><p>【译注】：正交性是指改变某个特性而不会影响到其他的特性。<a href="https://book.douban.com/subject/5387402/">《程序员修炼之道》</a>中讲了关于正交性的一个直升飞机坠毁的例子，讲得深入浅出很有画面感。</p></blockquote>
<p>为了方便，也写成：</p>
<p>[code language=&#8221;cpp&#8221;]<br />
timer.start(1000)；<br />
[/code]</p>
<p>类似地，对于<code>QRegExp</code>会是这样的代码：</p>
<p>[code language=&#8221;cpp&#8221;]<br />
QRegExp regExp;<br />
regExp.setCaseSensitive(Qt::CaseInsensitive);<br />
regExp.setPattern(&quot;.&quot;);<br />
regExp.setPatternSyntax(Qt::WildcardSyntax);<br />
[/code]</p>
<p>为实现这种类型的API，需要借助底层对象的懒创建。例如，对于<code>QRegExp</code>的例子，在不知道模式语法（pattern syntax）的情况下，在<code>setPattern()</code>中去解释<code>"."</code>就为时过早了。</p>
<p>属性之间常常有关联的；在这种情况下，我们必须小心处理。思考下面的问题：当前的风格（style）提供了『默认的图标尺寸』属性 vs. <code>QToolButton</code>的『<code>iconSize</code>』属性：</p>
<p>[code language=&#8221;cpp&#8221;]<br />
toolButton-&gt;setStyle(otherStyle);<br />
toolButton-&gt;iconSize();    // returns the default for otherStyle<br />
toolButton-&gt;setIconSize(QSize(52, 52));<br />
toolButton-&gt;iconSize();    // returns (52, 52)<br />
toolButton-&gt;setStyle(yetAnotherStyle);<br />
toolButton-&gt;iconSize();    // returns (52, 52)<br />
[/code]</p>
<p>提醒一下，一旦设置了<code>iconSize</code>，设置就会一直保持，即使改变当前的风格。这 <strong><em>很好</em></strong>。但有的时候需要能重置属性。有两种方法：</p>
<ol>
<li>传入一个特殊值（如<code>QSize()</code>、<code>-1</code>或者<code>Qt::Alignment(0)</code>）来表示『重置』</li>
<li>提供一个明确的重置方法，如<code>resetFoo()</code>和<code>unsetFoo()</code></li>
</ol>
<p>对于<code>iconSize</code>，使用<code>QSize()</code>（比如 <code>QSize(–1, -1)</code>）来表示『重置』就够用了。</p>
<p>在某些情况下，getter方法返回的结果与所设置的值不同。例如，虽然调用了<code>widget-&gt;setEnabled(true)</code>，但如果它的父widget处于disabled状态，那么<code>widget-&gt;isEnabled()</code>仍然返回的是<code>false</code>。这样是OK的，因为一般来说就是我们想要的检查结果（父widget处于disabled状态，里面的子widget也应该变为灰的不响应用户操作，就好像子widget自身处于disabled状态一样；与此同时，因为子widget记得在自己的内心深处是enabled状态的，只是一直等待着它的父widget变为enabled）。当然诸如这些都必须在文档中妥善地说明清楚。</p>
<h1>4. C++相关</h1>
<h2>4.1 值 vs. 对象</h2>
<h3>4.1.1 指针 vs. 引用</h3>
<p>指针（pointer）还是引用（reference）哪个是最好的输出参数（out-parameters）？</p>
<p>[code language=&#8221;cpp&#8221;]<br />
void getHsv(int *h, int *s, int *v) const;<br />
void getHsv(int &amp;h, int &amp;s, int &amp;v) const;<br />
[/code]</p>
<p>大多数C++书籍推荐尽可能使用引用，基于一个普遍的观点：引用比指针『更加安全和优雅』。与此相反，我们在开发Qt时倾向于指针，因为指针让用户代码可读性更好。比较下面例子：</p>
<p>[code language=&#8221;cpp&#8221;]<br />
color.getHsv(&amp;h, &amp;s, &amp;v);<br />
color.getHsv(h, s, v);<br />
[/code]</p>
<p>只有第一行代码清楚表达出<code>h</code>、<code>s</code>、<code>v</code>参数在函数调用中非常有可能会被修改。</p>
<p>这也就是说，编译器并不喜欢『出参』，所你应该在新的API中避免使用『出参』，而是返回一个结构体，如下所示：</p>
<p>[code language=&#8221;cpp&#8221;]<br />
struct Hsv { int hue, saturation, value };<br />
Hsv getHsv() const;<br />
[/code]</p>
<blockquote><p>【译注】：函数的『入参』和『出参』的混用会导致 API 接口语义的混乱，所以，使用指针，在调用的时候，实参需要加上“&amp;”，这样在代码阅读的时候，可以看到是一个『出参』，有利于代码阅读。（但是这样做，在函数内就需要判断指针是否为空的情况，因为引用是不需要判断的，所以，这是一种 trade-off）</p>
<p>另外，如果这样的参数过多的话，最好使用一个结构体来把数据打包，一方面，为一组返回值取个名字，另一方面，这样有利用接口的简单。</p></blockquote>
<h3>4.1.2 按常量引用传参 vs. 按值传参</h3>
<p>如果类型大于16字节，按常量引用传参。</p>
<p>如果类型有重型的（non-trivial）拷贝构造函数（copy-constructor）或是重型的析构函数（destructor），按常量引用传参以避免执行这些函数。</p>
<p>对于其它的类型通常应该按值传参。</p>
<p>示例：</p>
<p>[code language=&#8221;cpp&#8221;]<br />
void setAge(int age);<br />
void setCategory(QChar cat);<br />
void setName(QLatin1String name);</p>
<p>// const-ref is much faster than running copy-constructor and destructor<br />
void setAlarm(const QSharedPointer&lt;Alarm&gt; &amp;alarm);</p>
<p>// QDate, QTime, QPoint, QPointF, QSize, QSizeF, QRect<br />
// are good examples of other classes you should pass by value.<br />
[/code]</p>
<blockquote><p>【译注】：这是传引用和传值的差别了，因为传值会有对像拷贝，传引用则不会。所以，如果对像的构造比较重的话（换句话说，就是对像里的成员变量需要的内存比较大），这就会影响很多性能。所以，为了提高性能，最好是传引用。但是如果传入引用的话，会导致这个对象可能会被改变。所以传入const reference。</p></blockquote>
<h2>4.2 虚函数</h2>
<p>在C++中，当类的成员函数声明为virtual，主要是为了通过在子类重载此函数能够定制函数的行为。将函数声明为virtual的目的是为了让对这个函数已有的调用变成执行实际实例的代码路径。对于没有在类外部调用的函数声明成virtual，你应该事先非常慎重地思考过。</p>
<p>[code language=&#8221;cpp&#8221;]<br />
// QTextEdit in Qt 3: member functions that have no reason for being virtual<br />
virtual void resetFormat();<br />
virtual void setUndoDepth( int d );<br />
virtual void setFormat( QTextFormat *f, int flags );<br />
virtual void ensureCursorVisible();<br />
virtual void placeCursor( const QPoint &amp;pos;, QTextCursor **c = 0 );<br />
virtual void moveCursor( CursorAction action, bool select );<br />
virtual void doKeyboardAction( KeyboardAction action );<br />
virtual void removeSelectedText( int selNum = 0 );<br />
virtual void removeSelection( int selNum = 0 );<br />
virtual void setCurrentFont( const QFont &amp;f );<br />
virtual void setOverwriteMode( bool b ) { overWrite = b; }<br />
[/code]</p>
<p><code>QTextEdit</code>从Qt 3移植到Qt 4的时候，几乎所有的虚函数都被移除了。有趣的是（但在预料之中），并没有人对此有大的抱怨，为什么？因为Qt 3没用到<code>QTextEdit</code>的多态行为 —— 只有你会；简单地说，没有理由去继承<code>QTextEdit</code>并重写这些函数，除非你自己调用了这些方法。如果在Qt在外部你的应用程序你需要多态，你可以自己添加多态。</p>
<blockquote><p>【译注】：『多态』的目的只不过是为了实践 —— 『依赖于接口而不是实现』，也就是说，接口是代码抽像的一个非常重要的方式（在Java/Go中都有专门的接口声明语法）。所以，如果没有接口抽像，使用『多态』的意义也就不大了，因为也就没有必要使用『虚函数』了。</p></blockquote>
<h3>4.2.1 避免虚函数</h3>
<p>在Qt中，我们有很多理由尽量减少虚函数的数量。每一次对虚函数的调用会在函数调用链路中插入一个未掌控的节点（某种程度上使结果更无法预测），使得bug修复变得更复杂。用户在重写的虚函数中可以做很多疯狂的事：</p>
<ul>
<li>发送事件</li>
<li>发送信号</li>
<li>重新进入事件循环（例如，通过打开一个模态文件对话框）</li>
<li>删除对象（即触发『<code>delete this</code>』）</li>
</ul>
<p>还有其他很多原因要避免过度使用虚函数：</p>
<ul>
<li>添加、移动或是删除虚函数都带来二进制兼容问题（binary compatibility/BC）</li>
<li>重载虚函数并不容易</li>
<li>编译器几乎不能优化或内联（inline）对虚函数的调用</li>
<li>虚函数调用需要查找虚函数表（v-table），这比普通函数调用慢了2到3倍</li>
<li>虚函数使得类很难按值拷贝（尽管也可以按值拷贝，但是非常混乱并且不建议这样做）</li>
</ul>
<p>经验告诉我们，没有虚函数的类一般bug更少、维护成本也更低。</p>
<p>一般的经验法则是，除非我们以这个类作为工具集提供而且有很多用户来调用某个类的虚函数，否则这个函数九成不应该设计成虚函数。</p>
<blockquote><p>【译注】：</p>
<ol>
<li>使用虚函数时，你需要对编译器的内部行为非常清楚，否则，你会在使用虚函数时，觉得有好些『古怪』的问题发生。比如在创建数组对象的时候。</li>
<li>在C++中，会有一个基础类，这个基础类中已经实现好了很多功能，然后把其中的一些函数放给子类去修改和实现。这种方法在父类和子类都是一组开发人员维护时没有什么问题，但是如果这是两组开发人员，这就会带来很多问题了，就像Qt这样，子类完全无法控制，全世界的开发人员想干什么就干什么。所以，子类的代码和父类的代码在兼容上就会出现很多很多问题。所以，还是上面所说，其实，虚函数应该声明在接口的语义里（这就是设计模式的两个宗旨——依赖于接口，而不是实现；钟爱于组合，而不是继承。也是为什么Java和Go语言使用interface关键字的原因，C++在多态的语义上非常容易滥用）</li>
</ol>
</blockquote>
<h3>4.2.2 虚函数 vs. 拷贝</h3>
<p>多态对象（polymorphic objects）和值类型的类（value-type classes）两者很难协作好。</p>
<p>包含虚函数的类必须把析构函数声明为虚函数，以防止父类析构时没有清理子类的数据，导致内存泄漏。</p>
<p>如果要使一个类能够拷贝、赋值或按值比较，往往需要拷贝构造函数、赋值操作符（<code>operator =</code>）和相等操作符（<code>operator ==</code>）。</p>
<p>[code language=&#8221;cpp&#8221;]<br />
class CopyClass {<br />
public:<br />
    CopyClass();<br />
    CopyClass(const CopyClass &amp;other);<br />
    ~CopyClass();<br />
    CopyClass &amp;operator =(const CopyClass &amp;other);<br />
    bool operator ==(const CopyClass &amp;other) const;<br />
    bool operator !=(const CopyClass &amp;other) const;</p>
<p>    virtual void setValue(int v);<br />
};<br />
[/code]</p>
<p>如果继承<code>CopyClass</code>这个类，预料之外的事就已经在代码时酝酿了。一般情况下，如果没有虚成员函数和虚析构函数，就不能创建出可以多态的子类。然而，如果存在虚成员函数和虚析构函数，这突然变成了要有子类去继承的理由，而且开始变得复杂了。<strong><em>起初认为只要简单声明上虚操作符重载函数（virtual operators）。</em></strong> 但其实是走上了一条混乱和毁灭之路（破坏了代码的可读性）。看看下面的这个例子：</p>
<p>[code language=&#8221;cpp&#8221;]<br />
class OtherClass {<br />
public:<br />
    const CopyClass &amp;instance() const; // 这个方法返回的是什么？可以赋值什么？<br />
};<br />
[/code]</p>
<p>（这部份还未完成）</p>
<blockquote><p>【译注】：因为原文上说，这部份并没有完成，所以，我也没有搞懂原文具体也是想表达什么。不过，就标题而言，原文是想说，在多态的情况下拷贝对象所带来的问题？？</p></blockquote>
<h2>4.3 关于const</h2>
<p><strong><em>C++的关键词const表明了内容不会改变或是没有副作用。可以应用于简单的值、指针及指针所指的内容，也可以作为一个特别的属性应用于类的成员函数上，表示成员函数不能修改对象的状态。</em></strong></p>
<p>然而，const本身并没有提供太大的价值 —— 很多编程语言甚至没有类似const的关键词，但是却并没有因此产生问题。实际上，如果你不用函数重载，并在C++源代码用搜索并删除所有的const，几乎总能编译通过并且正常运行。尽量让使用的const保持实用有效，这点很重要。</p>
<p>让我们看一下在Qt的API设计中与const相关的场景。</p>
<h3>4.3.1 输入参数：const指针</h3>
<p>有输入指针参数的const成员函数，几乎总是const指针参数。</p>
<p>如果函数声明为const，意味着既没有副作用，也不会改变对象的可见状态。那为什么它需要一个没有const限定的输入参数呢？记住const类型的函数通常被其他const类型的函数调用，接收到的一般都是const指针（只要不主动const_cast，我们推荐尽量避免使用const_cast）</p>
<p>以前：</p>
<p>[code language=&#8221;cpp&#8221;]<br />
bool QWidget::isVisibleTo(QWidget *ancestor) const;<br />
bool QWidget::isEnabledTo(QWidget *ancestor) const;<br />
QPoint QWidget::mapFrom(QWidget *ancestor, const QPoint &amp;pos) const;<br />
[/code]</p>
<p><code>QWidget</code>声明了许多非const指针输入参数的const成员函数。注意，这些函数可以修改传入的参数，不能修改对象自己。使用这样的函数常常要借助const_cast转换。如果是const指针输入参数，就可以避免这样的转换了。</p>
<p>之后：</p>
<p>[code language=&#8221;cpp&#8221;]<br />
bool QWidget::isVisibleTo(const QWidget *ancestor) const;<br />
bool QWidget::isEnabledTo(const QWidget *ancestor) const;<br />
QPoint QWidget::mapFrom(const QWidget *ancestor, const QPoint &amp;pos) const;<br />
[/code]</p>
<p>注意，我们在<code>QGraphicsItem</code>中对此做了修正，但是<code>QWidget</code>要等到Qt 5:</p>
<p>[code language=&#8221;cpp&#8221;]<br />
bool isVisibleTo(const QGraphicsItem *parent) const;<br />
QPointF mapFromItem (const QGraphicsItem *item, const QPointF &amp;point) const;<br />
[/code]</p>
<h3>4.3.2 返回值：const值</h3>
<p>调用函数返回的非引用类型的结果，称之为右值（R-value）。</p>
<p>非类（non-class）的右值总是无cv限定类型（cv-unqualified type）。虽然从语法上讲，加上const也可以，但是没什么意义，因为鉴于访问权限这些值是不能改变的。多数现代编译器在编译这样的代码时会提示警告信息。</p>
<blockquote><p>【译注】：cv-qualified的类型（与cv-unqualified相反）是由const或者volatile或者volatile const限定的类型。详见<a href="http://en.cppreference.com/w/cpp/language/cv">cv (const and volatile) type qualifiers &#8211; C++语言参考</a></p></blockquote>
<p>当在类类型（class type）右值上添加const关键字，则禁止访问非const成员函数以及对成员的直接操作。</p>
<p>不加const则没有以上的限制，但几乎没有必要加上const，因为右值对象生存时间（life time）的结束一般在C++清理的时候（通俗的说，下一个分号地方），而对右值对象的修改随着右值对象的生存时间也一起结束了（也就是本条语句的执行完成的时候）。</p>
<p>示例：</p>
<p>[code language=&#8221;cpp&#8221;]<br />
struct Foo {<br />
    void setValue(int v) { value = v; }<br />
    int value;<br />
};</p>
<p>Foo foo() {<br />
    return Foo();<br />
}</p>
<p>const Foo cfoo() {<br />
    return Foo();<br />
}</p>
<p>int main() {<br />
    // The following does compile, foo() is non-const R-value which<br />
    // can&#8217;t be assigned to (this generally requires an L-value)<br />
    // but member access leads to a L-value:<br />
    foo().value = 1; // Ok, but temporary will be thrown away at the end of the full-expression.</p>
<p>    // The following does compile, foo() is non-const R-value which<br />
    // can&#8217;t be assigned to, but calling (even non-const) member<br />
    // function is fine:<br />
    foo().setValue(1); // Ok, but temporary will be thrown away at the end of the full-expression.</p>
<p>    // The following does _not_compile, foo() is &#8221;const&#8221; R-value<br />
    // with const member which member access can&#8217;t be assigned to:<br />
    cfoo().value = 1; // Not ok.</p>
<p>    // The following does _not_compile, foo() is &#8221;const&#8221; R-value,<br />
    // one cannot call non-const member functions:<br />
    cfoo().setValue(1); // Not ok<br />
}<br />
[/code]</p>
<blockquote><p>【译注】：上述的代码说明，如果返回值不是const的，代码可以顺利编译通过，然而并没有什么卵用，因为那个临时对像马上就被抛弃了。所以，这样的无用的代码最好还是在编译时报个错，以免当时头脑发热想错了，写了一段没用但还以为有用的代码。</p></blockquote>
<h3>4.3.3 返回值：非const的指针还是有const的指针</h3>
<p>谈到const函数应该返回非const的指针还是const指针这个话题时，多数人发现在C++中关于『const正确性』（const correctness）在概念上产生了分歧。 <em>问题起源是：<strong>const函数本身不能修改对象自身的状态，却可以返回成员的非const指针</strong>。返回指针这个简单动作本身既不会影响整个对象的可见状态，当然也不会改变这个函数职责范围内涉及的状态。但是，这却使得程序员可以间接访问并修改对象的状态。</em></p>
<p>下面的例子演示了通过返回非const指针的const函数绕开const约定（constness）的诸多方式中的一种：</p>
<p>[code language=&#8221;cpp&#8221;]<br />
QVariant CustomWidget::inputMethodQuery(Qt::InputMethodQuery query) const {<br />
    moveBy(10, 10); // doesn&#8217;t compile!<br />
    window()-&gt;childAt(mapTo(window(), rect().center()))-&gt;moveBy(10, 10); // compiles!<br />
}<br />
[/code]</p>
<p>返回const指针的函数正是保护以避免这些（可能是不期望的/没有预料到的）副作用，至少是在一定程度上。但哪个函数你会觉得更想返回const指针，或是不止一个函数？</p>
<p>若采用const正确（const-correct）的方法，每个返回某个成员的指针（或多个指向成员的指针）的const函数必须返回const指针。在实践中，很不幸这样的做法将导致无法使用的API：</p>
<p>[code language=&#8221;cpp&#8221;]<br />
QGraphicsScene scene;<br />
// … populate scene</p>
<p>foreach (const QGraphicsItem *item, scene.items()) {<br />
    item-&gt;setPos(qrand() % 500, qrand() % 500); // doesn&#8217;t compile! item is a const pointer<br />
}<br />
[/code]</p>
<p><code>QGraphicsScene::items()</code>是一个const函数，顺着思考看起来这个函数只应该返回const指针。</p>
<p>在Qt中，我们几乎只有非const的使用模式。我们选择的是实用路子： 相比滥用非const指针返回类型带来的问题，返回const指针更可能招致过分使用const_cast的问题。</p>
<h3>4.3.4 返回值：按值返回 还是 按const引用返回？</h3>
<p>若返回的是对象的拷贝，那么返回const引用是更直接的方案； 然而，这样的做法限制了后面想要对这个类的重构（refactor）。 （以<code>d-point</code>的典型做法（idiom）为例，我们可以在任何时候改变Qt类在内存表示（memory representation）；但却不能在不破坏二进制兼容性的情况下把改变函数的签名，返回值从<code>const QFoo &amp;</code>变为<code>QFoo</code>。） 基于这个原因，除去对运行速度敏感（speed is critical）而重构不是问题的个别情形（例如，<code>QList::at()</code>），我们一般返回<code>QFoo</code>而不是<code>const QFoo &amp;</code>。</p>
<blockquote><p>【译注】：参看《Effective C++》中条款23：Don&#8217;t try to return a reference when you must return an object</p></blockquote>
<h3>4.4.5 const vs. 对象的状态</h3>
<p>const正确性（Const correctness）的问题就像C圈子中vi与emacs的讨论，因为这个话题在很多地方都存在分歧（比如基于指针的函数）。</p>
<p>但通用准则是const函数不能改变类的可见状态。『状态』的意思是『自身以及涉及的职责』。这并不是指非const函数能够改变自身的私有成员，也不是指const函数改变不了。而是指函数是活跃的并存在可见的副作用（visible side effects）。const函数一般没有任何可见的副作用，比如：</p>
<p>[code language=&#8221;cpp&#8221;]<br />
QSize size = widget-&gt;sizeHint(); // const<br />
widget-&gt;move(10, 10); // not const<br />
[/code]</p>
<p>代理（delegate）负责在其它对象上绘制内容。 它的状态包括它的职责，因此包括在哪个对象做绘制这样的状态。 调用它的绘画行为必然会有副作用； 它改变了它绘制所在设备的外观（及其所关联的状态）。鉴于这些，<code>paint()</code>作为const函数并不合理。 进一步说，任何<code>paint()</code>或<code>QIcon</code>的<code>paint()</code>的视图函数是const函数也不合理。 没有人会从内部的const函数去调用<code>QIcon::paint()</code>，除非他想显式地绕开const这个特性。 如果是这种情况，使用const_cast会更好。</p>
<p>[code language=&#8221;cpp&#8221;]<br />
// QAbstractItemDelegate::paint is const<br />
void QAbstractItemDelegate::paint(QPainter **painter, const QStyleOptionViewItem &amp;option, const QModelIndex &amp;index) const</p>
<p>// QGraphicsItem::paint is not const<br />
void QGraphicsItem::paint(QPainter *painter, const QStyleOptionGraphicsItem option, QWidget *widget)<br />
[/code]</p>
<p>const关键字并不能按你期望的样子起作用。应该考虑将其移除而不是去重载const/非const函数。</p>
<h1>5. API的语义和文档</h1>
<p>当传值为<code>-1</code>的参数给函数，函数会是什么行为？有很多类似的问题……</p>
<p>是警告、致命错误还是其它？</p>
<p>API需要的是质量保证。API第一个版本一定是不对的；必须对其进行测试。 以阅读使用API的代码的方式编写用例，且验证这样代码是可读的。</p>
<p>还有其他的验证方法，比如</p>
<ul>
<li>让别人使用API（看了文档或是先不看文档都可以）</li>
<li>给类写文档（包含类的概述和每个函数）</li>
</ul>
<h1>6. 命名的艺术</h1>
<p>命名很可能是API设计中最重要的一个问题。类应该叫什么名字？成员函数应该叫什么名字？</p>
<h2>6.1 通用的命名规则</h2>
<p>有几个规则对于所有类型的命名都等同适用。第一个，之前已经提到过，不要使用缩写。即使是明显的缩写，比如把<code>previous</code>缩写成<code>prev</code>，从长远来看是回报是负的，因为用户必须要记住缩写词的含义。</p>
<p>如果API本身没有一致性，之后事情自然就会越来越糟；例如，Qt 3 中同时存在<code>activatePreviousWindow()</code>与<code>fetchPrev()</code>。恪守『不缩写』规则更容易地创建一致性的API。</p>
<p>另一个时重要但更微妙的准则是在设计类时应该保持子类名称空间的干净。在Qt 3中，此项准则并没有一直遵循。以<code>QToolButton</code>为例对此进行说明。如果调用<code>QToolButton</code>的 <code>name()</code>、<code>caption()</code>、<code>text()</code>或者<code>textLabel()</code>，你觉得会返回什么？用Qt设计器在<code>QToolButton</code>上自己先试试吧：</p>
<ul>
<li><code>name</code>属性是继承自<code>QObject</code>，返回内部的对象名称，用于调试和测试。</li>
<li><code>caption</code>属性继承自<code>QWidget</code>，返回窗口标题，对<code>QToolButton</code>来说毫无意义，因为它在创建的时候parent就存在了。</li>
<li><code>text</code>函数继承自<code>QButton</code>，一般用于按钮。当<code>useTextLabel</code>不为<code>true</code>，才用这个属性。</li>
<li><code>textLabel</code>属性在<code>QToolButton</code>内声明，当<code>useTextLabel</code>为<code>true</code>时显示在按钮上。</li>
</ul>
<p>为了可读性，在Qt 4中<code>QToolButton</code>的<code>name</code>属性改成了<code>objectName</code>，<code>caption</code>改成了<code>windowTitle</code>，删除了<code>textLabel</code>属性因为和<code>text</code>属性相同。</p>
<p>当你找不到好的命名时，写文档也是个很好方法：要做的就是尝试为各个条目（item）（如类、方法、枚举值等等）写文档，并用写下的第一句话作为启发。如果找不到一个确切的命名，往往说明这个条目是不该有的。如果所有尝试都失败了，并且你坚信这个概念是合理的，那么就发明一个新名字。像widget、event、focus和buddy这些命名就是在这一步诞生的。</p>
<blockquote><p>【译注】：写文档是一个非常好的习惯。写文档的过程其实就是在帮你梳理你的编程思路。很多时候，文档写着写着你就会发现要去改代码去了。除了上述的好处多，写文档还有更多的好处。比如，在写文档的过程中，你发现文字描述过于复杂了，这表明着你的代码或逻辑是复杂的，这就倒逼你去重构你的代码。所以 —— <strong>写文档其实就是写代码</strong>。</p></blockquote>
<h2>6.2 类的命名</h2>
<p>识别出类所在的分组，而不是为每个类都去找个完美的命名。例如，所有Qt 4的能感知模型（model-aware）的item view，类后缀都是<code>View</code>（<code>QListView</code>、<code>QTableView</code>、<code>QTreeView</code>），而相应的基于item（item-based）的类后缀是<code>Widget</code>（<code>QListWidget</code>、<code>QTableWidget</code>、<code>QTreeWidget</code>）。</p>
<h2>6.3 枚举类型及其值的命名</h2>
<p>声明枚举类型时，需要记住在C++中枚举值在使用时不会带上类型（与Java、C#不同）。下面的例子演示了枚举值命名得过于通用的危害：</p>
<p>[code language=&#8221;cpp&#8221;]<br />
namespace Qt<br />
{<br />
    enum Corner { TopLeft, BottomRight, &#8230; };<br />
    enum CaseSensitivity { Insensitive, Sensitive };<br />
    &#8230;<br />
};</p>
<p>tabWidget-&gt;setCornerWidget(widget, Qt::TopLeft);<br />
str.indexOf(&quot;$(QTDIR)&quot;, Qt::Insensitive);<br />
[/code]</p>
<p>在最后一行，<code>Insensitive</code>是什么意思？命名枚举类型的一个准则是在枚举值中至少重复此枚举类型名中的一个元素：</p>
<p>[code language=&#8221;cpp&#8221;]<br />
namespace Qt<br />
{<br />
    enum Corner { TopLeftCorner, BottomRightCorner, &#8230; };<br />
    enum CaseSensitivity { CaseInsensitive, CaseSensitive };<br />
    &#8230;<br />
};</p>
<p>tabWidget-&gt;setCornerWidget(widget, Qt::TopLeftCorner);<br />
str.indexOf(&quot;$(QTDIR)&quot;, Qt::CaseInsensitive);<br />
[/code]</p>
<p>当对枚举值进行或运算并作为某种标志（flag）时，传统的做法是把或运算的结果保存在int型的值中，但这不是类型安全的。Qt 4提供了一个模板类<code>QFlags</code>，其中的<code>T</code>是枚举类型。为了方便使用，Qt用<code>typedef</code>重新定义了<code>QFlag</code>类型，所以可以用<code>Qt::Alignment</code>代替<code>QFlags</code>。</p>
<p>习惯上，枚举类型命名用单数形式（因为它一次只能『持有』一个flag），而持有多个『flag』的类型用复数形式，例如：</p>
<p>[code language=&#8221;cpp&#8221;]<br />
enum RectangleEdge { LeftEdge, RightEdge, &#8230; };<br />
typedef QFlags&lt;RectangleEdge&gt; RectangleEdges;<br />
[/code]</p>
<p>在某些情形下，持有多个『flag』的类型命名用单数形式。对于这种情况，持有的枚举类型名称要求是以<code>Flag</code>为后缀：</p>
<p>[code language=&#8221;cpp&#8221;]<br />
enum AlignmentFlag { AlignLeft, AlignTop, &#8230; };<br />
typedef QFlags&lt;AlignmentFlag&gt; Alignment;<br />
[/code]</p>
<h2>6.4 函数和参数的命名</h2>
<p>函数命名的第一准则是可以从函数名看出来此函数是否有副作用。在Qt 3中，const函数<code>QString::simplifyWhiteSpace()</code>违反了此准则，因为它返回了一个<code>QString</code>而不是按名称暗示的那样，改变调用它的<code>QString</code>对象。在Qt 4中，此函数重命名为<code>QString::simplified()</code>。</p>
<p>虽然参数名不会出现在使用API的代码中，但是它们给程序员提供了重要信息。因为现代的IDE都会在写代码时显示参数名称，所以值得在头文件中给参数起一个恰当的名字并在文档中使用相同的名字。</p>
<h2>6.5 布尔类型的getter与setter方法的命名</h2>
<p>为<code>bool</code>属性的getter和setter方法命名总是很痛苦。getter应该叫做<code>checked()</code>还是<code>isChecked()</code>？<code>scrollBarsEnabled()</code>还是<code>areScrollBarEnabled()</code>？</p>
<p>Qt 4中，我们套用以下准则为getter命名：</p>
<ul>
<li>形容词以<code>is</code>为前缀，例子：
<ul>
<li><code>isChecked()</code></li>
<li><code>isDown()</code></li>
<li><code>isEmpty()</code></li>
<li><code>isMovingEnabled()</code></li>
</ul>
</li>
<li>然而，修饰名词的形容词没有前缀：
<ul>
<li><code>scrollBarsEnabled()</code>，而不是<code>areScrollBarsEnabled()</code></li>
</ul>
</li>
<li>动词没有前缀，也不使用第三人称(<code>-s</code>)：
<ul>
<li><code>acceptDrops()</code>，而不是<code>acceptsDrops()</code></li>
<li><code>allColumnsShowFocus()</code></li>
</ul>
</li>
<li>名词一般没有前缀：
<ul>
<li><code>autoCompletion()</code>，而不是<code>isAutoCompletion()</code></li>
<li><code>boundaryChecking()</code></li>
</ul>
</li>
<li>有的时候，没有前缀容易产生误导，这种情况下会加上<code>is</code>前缀：
<ul>
<li><code>isOpenGLAvailable()</code>，而不是<code>openGL()</code></li>
<li><code>isDialog()</code>，而不是<code>dialog()</code><br />
（一个叫做<code>dialog()</code>的函数，一般会被认为是返回<code>QDialog</code>。）</li>
</ul>
</li>
</ul>
<p>setter的名字由getter衍生，去掉了前缀后在前面加上了<code>set</code>；例如，<code>setDown()</code>与<code>setScrollBarsEnabled()</code>。</p>
<h1>7. 避免常见陷阱</h1>
<h2>7.1 简化的陷阱</h2>
<p>一个常见的误解是：实现需要写的代码越少，API就设计得越好。应该记住：代码只会写上几次，却要被反复阅读并理解。例如：</p>
<p>[code language=&#8221;cpp&#8221;]<br />
QSlider *slider = new QSlider(12, 18, 3, 13, Qt::Vertical, 0, &quot;volume&quot;);<br />
[/code]</p>
<p>这段代码比下面的读起来要难得多（甚至写起来也更难）：</p>
<p>[code language=&#8221;cpp&#8221;]<br />
QSlider *slider = new QSlider(Qt::Vertical);<br />
slider-&gt;setRange(12, 18);<br />
slider-&gt;setPageStep(3);<br />
slider-&gt;setValue(13);<br />
slider-&gt;setObjectName(&quot;volume&quot;);<br />
[/code]</p>
<blockquote><p>【译注】：在有IDE的自动提示的支持下，后者写起来非常方便，而前者还需要看相应的文档。</p></blockquote>
<h2>7.2 布尔参数的陷阱</h2>
<p>布尔类型的参数总是带来无法阅读的代码。给现有的函数增加一个<code>bool</code>型的参数几乎永远是一种错误的行为。仍以Qt为例，<code>repaint()</code>有一个<code>bool</code>类型的可选参数用于指定背景是否被擦除。可以写出这样的代码：</p>
<p>[code language=&#8221;cpp&#8221;]<br />
widget-&gt;repaint(false);<br />
[/code]</p>
<p>初学者很可能是这样理解的，『不要重新绘制！』，能有多少Qt用户真心知道下面3行是什么意思：</p>
<p>[code language=&#8221;cpp&#8221;]<br />
widget-&gt;repaint();<br />
widget-&gt;repaint(true);<br />
widget-&gt;repaint(false);<br />
[/code]</p>
<p>更好的API设计应该是这样的：</p>
<p>[code language=&#8221;cpp&#8221;]<br />
widget-&gt;repaint();<br />
widget-&gt;repaintWithoutErasing();<br />
[/code]</p>
<p>在Qt 4中，我们通过移除了重新绘制（repaint）而不擦除widget的能力来解决了此问题。Qt 4的双缓冲使这种特性被废弃。</p>
<p>还有更多的例子：</p>
<p>[code language=&#8221;cpp&#8221;]<br />
widget-&gt;setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding, true);<br />
textEdit-&gt;insert(&quot;Where&#8217;s Waldo?&quot;, true, true, false);<br />
QRegExp rx(&quot;moc_***.c??&quot;, false, true);<br />
[/code]</p>
<p>一个明显的解决方案是<code>bool</code>类型改成枚举类型。我们在Qt 4的<code>QString</code>中就是这么做的。对比效果如下：</p>
<p>[code language=&#8221;cpp&#8221;]<br />
str.replace(&quot;%USER%&quot;, user, false);               // Qt 3<br />
str.replace(&quot;%USER%&quot;, user, Qt::CaseInsensitive); // Qt 4<br />
[/code]</p>
<blockquote><p>【译注】：关于这个条目可以看看 CoolShell 这篇文章一些展开的讨论： <a href="https://coolshell.cn/articles/5444.html" rel="nofollow">千万不要把 BOOL 设计成函数参数</a>。</p></blockquote>
<h1>8. 案例研究</h1>
<h2>8.1 <code>QProgressBar</code></h2>
<p>为了展示上文各种准则的实际应用。我们来研究一下Qt 3中<code>QProgressBar</code>的API，并与Qt 4中对应的API作比较。在Qt 3中：</p>
<p>[code language=&#8221;cpp&#8221;]<br />
class QProgressBar : public QWidget<br />
{<br />
    &#8230;<br />
public:<br />
    int totalSteps() const;<br />
    int progress() const;</p>
<p>    const QString &amp;progressString() const;<br />
    bool percentageVisible() const;<br />
    void setPercentageVisible(bool);</p>
<p>    void setCenterIndicator(bool on);<br />
    bool centerIndicator() const;</p>
<p>    void setIndicatorFollowsStyle(bool);<br />
    bool indicatorFollowsStyle() const;</p>
<p>public slots:<br />
    void reset();<br />
    virtual void setTotalSteps(int totalSteps);<br />
    virtual void setProgress(int progress);<br />
    void setProgress(int progress, int totalSteps);</p>
<p>protected:<br />
    virtual bool setIndicator(QString &amp;progressStr,<br />
                              int progress,<br />
                              int totalSteps);<br />
    &#8230;<br />
};<br />
[/code]</p>
<p>该API相当的复杂和不一致；例如，<code>reset()</code>、<code>setTotalSteps()</code>、<code>setProgress()</code>是紧密联系的，但方法的命名并没明确地表达出来。</p>
<p>改善此API的关键是抓住<code>QProgressBar</code>与Qt 4的<code>QAbstractSpinBox</code>及其子类<code>QSpinBox</code>、<code>QSlider</code>、<code>QDail</code>之间的相似性。怎么做？把<code>progress</code>、<code>totalSteps</code>替换为<code>minimum</code>、<code>maximum</code>和<code>value</code>。增加一个<code>valueChanged()</code>消息，再增加一个<code>setRange()</code>函数。</p>
<p>进一步可以观察到<code>progressString</code>、<code>percentage</code>与<code>indicator</code>其实是一回事，即是显示在进度条上的文本。通常这个文本是个百分比，但是可通过<code>setIndicator()</code>设置为任何内容。以下是新的API：</p>
<p>[code language=&#8221;cpp&#8221;]<br />
virtual QString text() const;<br />
void setTextVisible(bool visible);<br />
bool isTextVisible() const;<br />
[/code]</p>
<p>默认情况下，显示文本是百分比指示器（percentage indicator），通过重写<code>text()</code>方法来定制行为。</p>
<p>Qt 3的<code>setCenterIndicator()</code>与<code>setIndicatorFollowsStyle()</code>是两个影响对齐方式的函数。他们可被一个<code>setAlignment()</code>函数代替：</p>
<p>[code language=&#8221;cpp&#8221;]<br />
void setAlignment(Qt::Alignment alignment);<br />
[/code]</p>
<p>如果开发者未调用<code>setAlignment()</code>，那么对齐方式由风格决定。对于基于<code>Motif</code>的风格，文字内容在中间显示；对于其他风格，在右侧显示。</p>
<p>下面是改善后的<code>QProgressBar API</code>:</p>
<p>[code language=&#8221;cpp&#8221;]<br />
class QProgressBar : public QWidget<br />
{<br />
    &#8230;<br />
public:<br />
    void setMinimum(int minimum);<br />
    int minimum() const;<br />
    void setMaximum(int maximum);<br />
    int maximum() const;<br />
    void setRange(int minimum, int maximum);<br />
    int value() const;</p>
<p>    virtual QString text() const;<br />
    void setTextVisible(bool visible);<br />
    bool isTextVisible() const;<br />
    Qt::Alignment alignment() const;<br />
    void setAlignment(Qt::Alignment alignment);</p>
<p>public slots:<br />
    void reset();<br />
    void setValue(int value);</p>
<p>signals:<br />
    void valueChanged(int value);<br />
    &#8230;<br />
};<br />
[/code]</p>
<h2>8.2 <code>QAbstractPrintDialog</code> &amp; <code>QAbstractPageSizeDialog</code></h2>
<p>Qt 4.0有2个幽灵类<code>QAbstractPrintDialog</code>和<code>QAbstractPageSizeDialog</code>，作为 <code>QPrintDialog</code>和<code>QPageSizeDialog</code>类的父类。这2个类完全没有用，因为Qt的API没有是<code>QAbstractPrint-</code>或是<code>-PageSizeDialog</code>指针作为参数并执行操作。通过篡改qdoc（Qt文档），我们虽然把这2个类隐藏起来了，却成了无用抽象类的典型案例。</p>
<p>这不是说，<strong><em>好</em></strong> 的抽象是错的，<code>QPrintDialog</code>应该是需要有个工厂或是其它改变的机制 —— 证据就是它声明中的<code>#ifdef QTOPIA_PRINTDIALOG</code>。</p>
<h2>8.3 <code>QAbstractItemModel</code></h2>
<p>关于模型/视图（model/view）问题的细节在相应的文档中已经说明得很好了，但作为一个重要的总结这里还需要强调一下：抽象类不应该仅是所有可能子类的并集（union）。这样『合并所有』的父类几乎不可能是一个好的方案。<code>QAbstractItemModel</code>就犯了这个错误 —— 它实际上就是个<code>QTreeOfTablesModel</code>，结果导致了错综复杂（complicated）的API，而这样的API要让 <strong><em>所有本来设计还不错的子类</em></strong> 去继承。</p>
<p>仅仅增加抽象是不会自动就把API变得更好的。</p>
<h2>8.4 <code>QLayoutIterator</code> &amp; <code>QGLayoutIterator</code></h2>
<p>在Qt 3，创建自定义的布局类需要同时继承<code>QLayout</code>和<code>QGLayoutIterator</code>（命名中的<code>G</code>是指Generic（通用））。<code>QGLayoutIterator</code>子类的实例指针会包装成<code>QLayoutIterator</code>，这样用户可以像和其它的迭代器（iterator）类一样的方式来使用。通过<code>QLayoutIterator</code>可以写出下面这样的代码：</p>
<p>[code language=&#8221;cpp&#8221;]<br />
QLayoutIterator it = layout()-&gt;iterator();<br />
QLayoutItem **child;<br />
while ((child = it.current()) != 0) {<br />
    if (child-&gt;widget() == myWidget) {<br />
        it.takeCurrent();<br />
        return;<br />
    }<br />
    ++it;<br />
}<br />
[/code]</p>
<p>在Qt 4，我们干掉了<code>QGLayoutIterator</code>类（以及用于盒子布局和格子布局的内部子类），转而是让<code>QLayout</code>的子类重写<code>itemAt()</code>、<code>takeAt()</code>和<code>count()</code>。</p>
<h2>8.5 <code>QImageSink</code></h2>
<p>Qt 3有一整套类用来把完成增量加载的图片传递给一个动画 —— <code>QImageSource</code>/<code>Sink</code>/<code>QASyncIO</code>/<code>QASyncImageIO</code>。由于这些类之前只是用于启用动画的<code>QLabel</code>，完全过度设计了（overkill）。</p>
<p>从中得到的教训就是：对于那些未来可能的还不明朗的需求，不要过早地增加抽象设计。当需求真的出现时，比起一个复杂的系统，在简单的系统新增需求要容易得多。<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/5444.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/14.jpg" alt="千万不要把 bool 设计成函数参数" width="150" height="150" /></a><a href="https://coolshell.cn/articles/5444.html" class="wp_rp_title">千万不要把 bool 设计成函数参数</a></li><li ><a href="https://coolshell.cn/articles/4758.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/30.jpg" alt="如何写出无法维护的代码" width="150" height="150" /></a><a href="https://coolshell.cn/articles/4758.html" class="wp_rp_title">如何写出无法维护的代码</a></li><li ><a href="https://coolshell.cn/articles/18360.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2018/05/300x262-150x150.jpg" alt="程序员练级攻略（2018)  与我的专栏" width="150" height="150" /></a><a href="https://coolshell.cn/articles/18360.html" class="wp_rp_title">程序员练级攻略（2018)  与我的专栏</a></li><li ><a href="https://coolshell.cn/articles/17680.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2017/02/gitlab-600-150x150.jpg" alt="从Gitlab误删除数据库想到的" width="150" height="150" /></a><a href="https://coolshell.cn/articles/17680.html" class="wp_rp_title">从Gitlab误删除数据库想到的</a></li><li ><a href="https://coolshell.cn/articles/17459.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2016/08/HighAvailability-BK-150x150.png" alt="关于高可用的系统" width="150" height="150" /></a><a href="https://coolshell.cn/articles/17459.html" class="wp_rp_title">关于高可用的系统</a></li><li ><a href="https://coolshell.cn/articles/12052.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/29.jpg" alt="Leetcode 编程训练" width="150" height="150" /></a><a href="https://coolshell.cn/articles/12052.html" class="wp_rp_title">Leetcode 编程训练</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/18024.html">API设计原则 – Qt官网的设计实践总结</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/18024.html/feed</wfw:commentRss>
			<slash:comments>26</slash:comments>
		
		
			</item>
		<item>
		<title>如何重构“箭头型”代码</title>
		<link>https://coolshell.cn/articles/17757.html</link>
					<comments>https://coolshell.cn/articles/17757.html#comments</comments>
		
		<dc:creator><![CDATA[陈皓]]></dc:creator>
		<pubDate>Wed, 05 Apr 2017 10:07:14 +0000</pubDate>
				<category><![CDATA[C/C++语言]]></category>
		<category><![CDATA[程序设计]]></category>
		<category><![CDATA[编程语言]]></category>
		<category><![CDATA[Code Review]]></category>
		<category><![CDATA[Programming]]></category>
		<category><![CDATA[Refactory]]></category>
		<guid isPermaLink="false">http://coolshell.cn/?p=17757</guid>

					<description><![CDATA[<p>本文主要起因是，一次在微博上和朋友关于嵌套好几层的if-else语句的代码重构的讨论（微博原文），在微博上大家有各式各样的问题和想法。按道理来说这些都是编程的基...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/17757.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/17757.html">如何重构“箭头型”代码</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script>本文主要起因是，一次在微博上和朋友关于嵌套好几层的if-else语句的代码重构的讨论（<a href="http://weibo.com/1401880315/ECmCW0oy2" target="_blank" rel="noopener noreferrer">微博原文</a>），在微博上大家有各式各样的问题和想法。按道理来说这些都是编程的基本功，似乎不太值得写一篇文章，不过我觉得很多东西可以从一个简单的东西出发，到达本质，所以，我觉得有必要在这里写一篇的文章。不一定全对，只希望得到更多的讨论，因为有了更深入的讨论才能进步。</p>
<p>文章有点长，我在文章最后会给出相关的思考和总结陈词，你可以跳到结尾。</p>
<p>所谓箭头型代码，基本上来说就是下面这个图片所示的情况。</p>
<p><img decoding="async" loading="lazy" class="aligncenter wp-image-17758 size-full" src="https://coolshell.cn/wp-content/uploads/2017/04/IMG_7411.jpg" alt="" width="720" height="511" srcset="https://coolshell.cn/wp-content/uploads/2017/04/IMG_7411.jpg 720w, https://coolshell.cn/wp-content/uploads/2017/04/IMG_7411-300x213.jpg 300w, https://coolshell.cn/wp-content/uploads/2017/04/IMG_7411-380x270.jpg 380w" sizes="(max-width: 720px) 100vw, 720px" /></p>
<p>那么，这样“箭头型”的代码有什么问题呢？看上去也挺好看的，有对称美。但是……</p>
<p>关于箭头型代码的问题有如下几个：</p>
<p><span id="more-17757"></span></p>
<p>1）我的显示器不够宽，箭头型代码缩进太狠了，需要我来回拉水平滚动条，这让我在读代码的时候，相当的不舒服。</p>
<p>2）除了宽度外还有长度，有的代码的<code>if-else</code>里的<code>if-else</code>里的<code>if-else</code>的代码太多，读到中间你都不知道中间的代码是经过了什么样的层层检查才来到这里的。</p>
<p>总而言之，<strong>“箭头型代码”如果嵌套太多，代码太长的话，会相当容易让维护代码的人（包括自己）迷失在代码中，因为看到最内层的代码时，你已经不知道前面的那一层一层的条件判断是什么样的，代码是怎么运行到这里的，所以，箭头型代码是非常难以维护和Debug的</strong>。</p>
<h4>微博上的案例 与 Guard Clauses</h4>
<p>OK，我们先来看一下微博上的那个示例，代码量如果再大一点，嵌套再多一点，你很容易会在条件中迷失掉（下面这个示例只是那个“大箭头”下的一个小箭头）</p>
<pre class="EnlighterJSRAW" data-enlighter-language="cpp">
FOREACH(Ptr&lt;WfExpression&gt;, argument, node-&gt;arguments) {
    int index = manager-&gt;expressionResolvings.Keys().IndexOf(argument.Obj());
    if (index != -1) {
        auto type = manager-&gt;expressionResolvings.Values()[index].type;
        if (! types.Contains(type.Obj())) {
            types.Add(type.Obj());
            if (auto group = type-&gt;GetTypeDescriptor()-&gt;GetMethodGroupByName(L&quot;CastResult&quot;, true)) {
                int count = group-&gt;GetMethodCount();
                for (int i = 0; i &lt; count; i++) { auto method = group-&gt;GetMethod(i);
                    if (method-&gt;IsStatic()) {
                        if (method-&gt;GetParameterCount() == 1 &amp;&amp;
                            method-&gt;GetParameter(0)-&gt;GetType()-&gt;GetTypeDescriptor() == description::GetTypeDescriptor&lt;DescriptableObject&gt;() &amp;&amp;
                            method-&gt;GetReturn()-&gt;GetTypeDescriptor() != description::GetTypeDescriptor&lt;void&gt;() ) {
                            symbol-&gt;typeInfo = CopyTypeInfo(method-&gt;GetReturn());
                            break;
                        }
                    }
                }
            }
        }
    }
}
</pre>
<p>上面这段代码，可以把条件反过来写，然后就可以把箭头型的代码解掉了，重构的代码如下所示：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="cpp">
FOREACH(Ptr&lt;WfExpression&gt;, argument, node-&gt;arguments) {
    int index = manager-&gt;expressionResolvings.Keys().IndexOf(argument.Obj());
    if (index == -1)  continue;
    
    auto type = manager-&gt;expressionResolvings.Values()[index].type;
    if ( types.Contains(type.Obj()))  continue;
    
    types.Add(type.Obj());

    auto group = type-&gt;GetTypeDescriptor()-&gt;GetMethodGroupByName(L&quot;CastResult&quot;, true);
    if  ( ! group ) continue;
 
    int count = group-&gt;GetMethodCount();
    for (int i = 0; i &lt; count; i++) { auto method = group-&gt;GetMethod(i);
        if (! method-&gt;IsStatic()) continue;
       
        if ( method-&gt;GetParameterCount() == 1 &amp;&amp;
               method-&gt;GetParameter(0)-&gt;GetType()-&gt;GetTypeDescriptor() == description::GetTypeDescriptor&lt;DescriptableObject&gt;() &amp;&amp;
               method-&gt;GetReturn()-&gt;GetTypeDescriptor() != description::GetTypeDescriptor&lt;void&gt;() ) {
            symbol-&gt;typeInfo = CopyTypeInfo(method-&gt;GetReturn());
            break;
        }
    }
}
</pre>
<p>这种代码的重构方式叫 <strong>Guard Clauses</strong></p>
<ul>
<li><a href="https://martinfowler.com/" target="_blank" rel="noopener noreferrer">Martin Fowler</a> 的 Refactoring 的网站上有相应的说明《<a href="https://refactoring.com/catalog/replaceNestedConditionalWithGuardClauses.html" target="_blank" rel="noopener noreferrer">Replace Nested Conditional with Guard Clauses</a>》。</li>
</ul>
<ul>
<li><a href="https://blog.codinghorror.com/" target="_blank" rel="noopener noreferrer">Coding Horror</a> 上也有一篇文章讲了这种重构的方式 —— 《<a href="https://blog.codinghorror.com/flattening-arrow-code/" target="_blank" rel="noopener noreferrer">Flattening Arrow Code</a>》</li>
</ul>
<ul>
<li><a href="http://stackoverflow.com/" target="_blank" rel="noopener noreferrer">StackOverflow</a> 上也有相关的问题说了这种方式 —— 《<a href="http://stackoverflow.com/questions/356121/refactor-nested-if-statement-for-clarity" target="_blank" rel="noopener noreferrer">Refactor nested IF statement for clarity</a>》</li>
</ul>
<p>这里的思路其实就是，<strong>让出错的代码先返回，前面把所有的错误判断全判断掉，然后就剩下的就是正常的代码了</strong>。</p>
<h4>抽取成函数</h4>
<p>微博上有些人说，continue 语句破坏了阅读代码的通畅，我觉得他们一定没有好好读这里面的代码，其实，我们可以看到，所有的 if 语句都是在判断是否出错的情况，所以，在维护代码的时候，你可以完全不理会这些 if 语句，因为都是出错处理的，而剩下的代码都是正常的功能代码，反而更容易阅读了。当然，一定有不是上面代码里的这种情况，那么，不用continue ，我们还能不能重构呢？</p>
<p>当然可以，抽成函数：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="cpp">
bool CopyMethodTypeInfo(auto &amp;method, auto &amp;group, auto &amp;symbol) 
{
    if (! method-&gt;IsStatic()) {
        return true;
    }
    if ( method-&gt;GetParameterCount() == 1 &amp;&amp;
           method-&gt;GetParameter(0)-&gt;GetType()-&gt;GetTypeDescriptor() == description::GetTypeDescriptor&lt;DescriptableObject&gt;() &amp;&amp;
           method-&gt;GetReturn()-&gt;GetTypeDescriptor() != description::GetTypeDescriptor&lt;void&gt;() ) {
        symbol-&gt;typeInfo = CopyTypeInfo(method-&gt;GetReturn());
        return false;
    }
    return true;
}

void ExpressionResolvings(auto &amp;manager, auto &amp;argument, auto &amp;symbol) 
{
    int index = manager-&gt;expressionResolvings.Keys().IndexOf(argument.Obj());
    if (index == -1) return;
    
    auto type = manager-&gt;expressionResolvings.Values()[index].type;
    if ( types.Contains(type.Obj())) return;

    types.Add(type.Obj());
    auto group = type-&gt;GetTypeDescriptor()-&gt;GetMethodGroupByName(L&quot;CastResult&quot;, true);
    if  ( ! group ) return;

    int count = group-&gt;GetMethodCount();
    for (int i = 0; i &lt; count; i++) { auto method = group-&gt;GetMethod(i);
        if ( ! CopyMethodTypeInfo(method, group, symbol) ) break;
    }
}

...
...
FOREACH(Ptr&lt;WfExpression&gt;, argument, node-&gt;arguments) {
    ExpressionResolvings(manager, arguments, symbol)
}
...
...
</pre>
<p>你发出现，抽成函数后，代码比之前变得更容易读和更容易维护了。不是吗？</p>
<p>有人说：“如果代码不共享，就不要抽取成函数！”，持有这个观点的人太死读书了。函数是代码的封装或是抽象，并不一定用来作代码共享使用，函数用于屏蔽细节，让其它代码耦合于接口而不是细节实现，这会让我们的代码更为简单，简单的东西都能让人易读也易维护。这才是函数的作用。</p>
<h4>嵌套的 if 外的代码</h4>
<p>微博上还有人问，原来的代码如果在各个 if 语句后还有要执行的代码，那么应该如何重构。比如下面这样的代码。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="cpp">
//原版
for(....) {
    do_before_cond1()
    if (cond1) {
        do_before_cond2();
        if (cond2) {
            do_before_cond3();
            if (cond3) {
                do_something();
            }
            do_after_cond3();
        }
        do_after_cond2();
    }
    do_after_cond1();
}</pre>
<p>上面这段代码中的那些 <code>do_after_condX()</code> 是无论条件成功与否都要执行的。所以，我们拉平后的代码如下所示：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="cpp">
//重构第一版
for(....) {
    do_before_cond1();
    if ( !cond1 ) {
        do_after_cond1();
        continue
    } 
    do_after_cond1();

    do_before_cond2();
    if ( !cond2 ) { 
        do_after_cond2();
        continue;
    }
    do_after_cond2();

    do_before_cond3();
    if ( !cond3 ) {
        do_after_cond3();
        continue;
    }
    do_after_cond3();

    do_something();  
}</pre>
<p>你会发现，上面的 <code>do_after_condX</code> 出现了两份。<strong>如果 if 语句块中的代码改变了某些<code>do_after_condX</code>依赖的状态，那么这是最终版本。</strong></p>
<p>但是，如果它们之前没有依赖关系的话，根据 DRY 原则，我们就可以只保留一份，那么直接掉到 if 条件前就好了，如下所示：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="cpp">
//重构第二版
for(....) {
    do_before_cond1();
    do_after_cond1();
    if ( !cond1 ) continue;
 
    do_before_cond2();
    do_after_cond2();
    if ( !cond2 ) continue;

    do_before_cond3();
    do_after_cond3();
    if ( !cond3 ) continue;

    do_something();  
}</pre>
<p>此时，你会说，我靠，居然，改变了执行的顺序，把条件放到 <code>do_after_condX()</code> 后面去了。这会不会有问题啊？</p>
<p>其实，你再分析一下之前的代码，你会发现，本来，cond1 是判断 do_before_cond1() 是否出错的，如果有成功了，才会往下执行。而 do_after_cond1() 是无论如何都要执行的。从逻辑上来说，do_after_cond1()其实和do_before_cond1()的执行结果无关，而 cond1 却和是否去执行 do_before_cond2() 相关了。如果我把断行变成下面这样，反而代码逻辑更清楚了。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="cpp">
//重构第三版
for(....) {

    do_before_cond1();
    do_after_cond1();


    if ( !cond1 ) continue;  // &lt;-- cond1 成了是否做第二个语句块的条件
    do_before_cond2();
    do_after_cond2();

    if ( !cond2 ) continue; // &lt;-- cond2 成了是否做第三个语句块的条件
    do_before_cond3();
    do_after_cond3();

    if ( !cond3 ) continue; //&lt;-- cond3 成了是否做第四个语句块的条件
    do_something(); 
 
}
</pre>
<p>于是乎，在未来维护代码的时候，维护人一眼看上去就明白，代码在什么时候会执行到哪里。 这个时候，你会发现，把这些语句块抽成函数，代码会干净的更多，再重构一版：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="cpp">
//重构第四版
bool do_func3() {
   do_before_cond2();
   do_after_cond2();
   return cond3;
}

bool do_func2() {
   do_before_cond2();
   do_after_cond2();
   return cond2;
}

bool do_func1() {
   do_before_cond1();
   do_after_cond1();
   return cond1;
}

// for-loop 你可以重构成这样
for (...) {
    bool cond = do_func1();
    if (cond) cond = do_func2();
    if (cond) cond = do_func3();
    if (cond) do_something();
}

// for-loop 也可以重构成这样
for (...) {
    if ( ! do_func1() ) continue;
    if ( ! do_func2() ) continue;
    if ( ! do_func3() ) continue;
    do_something();
}
</pre>
<p>上面，我给出了两个版本的for-loop，你喜欢哪个？我喜欢第二个。这个时候，因为for-loop里的代码非常简单，就算你不喜欢 continue ，这样的代码阅读成本已经很低了。</p>
<h4>状态检查嵌套</h4>
<p>接下来，我们再来看另一个示例。下面的代码的伪造了一个场景——把两个人拉到一个一对一的聊天室中，因为要检查双方的状态，所以，代码可能会写成了“箭头型”。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="cpp">
int ConnectPeer2Peer(Conn *pA, Conn* pB, Manager *manager)
{
    if ( pA-&gt;isConnected() ) {
        manager-&gt;Prepare(pA);
        if ( pB-&gt;isConnected() ) {
            manager-&gt;Prepare(pB);
            if ( manager-&gt;ConnectTogther(pA, pB) ) {
                pA-&gt;Write(&quot;connected&quot;);
                pB-&gt;Write(&quot;connected&quot;);
                return S_OK;
            }else{
                return S_ERROR;
            }

        }else {
            pA-&gt;Write(&quot;Peer is not Ready, waiting...&quot;);
            return S_RETRY;
        }
    }else{
        if ( pB-&gt;isConnected() ) {
            manager-&gt;Prepare();
            pB-&gt;Write(&quot;Peer is not Ready, waiting...&quot;);
            return S_RETRY;
        }else{
            pA-&gt;Close();
            pB-&gt;Close();
            return S_ERROR;
        }
    }
    //Shouldn't be here!
    return S_ERROR;
}</pre>
<p>重构上面的代码，我们可以先分析一下上面的代码，说明了，上面的代码就是对 PeerA 和 PeerB 的两个状态 “连上”， “未连上” 做组合 “状态” （注：实际中的状态应该比这个还要复杂，可能还会有“断开”、“错误”……等等状态）， 于是，我们可以把代码写成下面这样，合并上面的嵌套条件，对于每一种组合都做出判断。这样一来，逻辑就会非常的干净和清楚。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="cpp">
int ConnectPeer2Peer(Conn *pA, Conn* pB, Manager *manager)
{
    if ( pA-&gt;isConnected() ) {
        manager-&gt;Prepare(pA);
    }

    if ( pB-&gt;isConnected() ) {
        manager-&gt;Prepare(pB);
    }

    // pA = YES &amp;&amp; pB = NO
    if (pA-&gt;isConnected() &amp;&amp; ! pB-&gt;isConnected()  ) {
        pA-&gt;Write(&quot;Peer is not Ready, waiting&quot;);
        return S_RETRY;
    // pA = NO &amp;&amp; pB = YES
    }else if ( !pA-&gt;isConnected() &amp;&amp; pB-&gt;isConnected() ) {
        pB-&gt;Write(&quot;Peer is not Ready, waiting&quot;);
        return S_RETRY;
    // pA = YES &amp;&amp; pB = YES
    }else if (pA-&gt;isConnected() &amp;&amp; pB-&gt;isConnected()  ) {
        if ( ! manager-&gt;ConnectTogther(pA, pB) ) {
            return S_ERROR;
        }
        pA-&gt;Write(&quot;connected&quot;);
        pB-&gt;Write(&quot;connected&quot;);
        return S_OK;
    }

    // pA = NO, pB = NO
    pA-&gt;Close();
    pB-&gt;Close();
    return S_ERROR;
}</pre>
<h4>延伸思考</h4>
<p>对于 <code>if-else</code> 语句来说，一般来说，就是检查两件事：<strong>错误</strong> 和 <strong>状态</strong>。</p>
<h5>检查错误</h5>
<p>对于检查错误来说，使用 Guard Clauses 会是一种标准解，但我们还需要注意下面几件事：</p>
<p style="padding-left: 30px;">1）当然，出现错误的时候，还会出现需要释放资源的情况。你可以使用 <code>goto fail;</code> 这样的方式，但是最优雅的方式应该是C++面向对象式的 RAII 方式。</p>
<p style="padding-left: 30px;">2）以错误码返回是一种比较简单的方式，这种方式有很一些问题，比如，如果错误码太多，判断出错的代码会非常复杂，另外，正常的代码和错误的代码会混在一起，影响可读性。所以，在更为高组的语言中，使用 <code>try-catch</code> 异常捕捉的方式，会让代码更为易读一些。</p>
<h5>检查状态</h5>
<p>对于检查状态来说，实际中一定有更为复杂的情况，比如下面几种情况：</p>
<p style="padding-left: 30px;">1）像TCP协议中的两端的状态变化。</p>
<p style="padding-left: 30px;">2）像shell各个命令的命令选项的各种组合。</p>
<p style="padding-left: 30px;">3）像游戏中的状态变化（一棵非常复杂的状态树）。</p>
<p style="padding-left: 30px;">4）像语法分析那样的状态变化。</p>
<p>对于这些复杂的状态变化，其本上来说，你需要先定义一个状态机，或是一个子状态的组合状态的查询表，或是一个状态查询分析树。</p>
<p><strong>写代码时，代码的运行中的控制状态或业务状态是会让你的代码流程变得混乱的一个重要原因，重构“箭头型”代码的一个很重要的工作就是重新梳理和描述这些状态的变迁关系</strong>。</p>
<h4>总结</h4>
<p>好了，下面总结一下，把“箭头型”代码重构掉的几个手段如下：</p>
<p>1）<strong>使用 Guard Clauses </strong>。 尽可能的让出错的先返回， 这样后面就会得到干净的代码。</p>
<p>2）<strong>把条件中的语句块抽取成函数</strong>。 有人说：“如果代码不共享，就不要抽取成函数！”，持有这个观点的人太死读书了。函数是代码的封装或是抽象，并不一定用来作代码共享使用，函数用于屏蔽细节，让其它代码耦合于接口而不是细节实现，这会让我们的代码更为简单，简单的东西都能让人易读也易维护，<strong>写出让人易读易维护的代码才是重构代码的初衷</strong>！</p>
<p>3）<strong>对于出错处理，使用try-catch异常处理和<a href="http://stackoverflow.com/questions/2321511/what-is-meant-by-resource-acquisition-is-initialization-raii" target="_blank" rel="noopener noreferrer">RAII机制</a></strong>。返回码的出错处理有很多问题，比如：A) 返回码可以被忽略，B) 出错处理的代码和正常处理的代码混在一起，C) 造成函数接口污染，比如像atoi()这种错误码和返回值共用的糟糕的函数。</p>
<p>4）<strong>对于多个状态的判断和组合，如果复杂了，可以使用“组合状态表”，或是状态机加Observer的状态订阅的设计模式</strong>。这样的代码即解了耦，也干净简单，同样有很强的扩展性。</p>
<p>5） <strong>重构“箭头型”代码其实是在帮你重新梳理所有的代码和逻辑，这个过程非常值得为之付出</strong>。重新整思路去想尽一切办法简化代码的过程本身就可以让人成长。</p>
<p>（全文完）<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/19612.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2019/07/1920px-Margaret_Hamilton_-_restoration-e1563697198766-1-150x150.jpg" alt="50年前的登月程序和程序员有多硬核" width="150" height="150" /></a><a href="https://coolshell.cn/articles/19612.html" class="wp_rp_title">50年前的登月程序和程序员有多硬核</a></li><li ><a href="https://coolshell.cn/articles/17929.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2017/06/go-hardhat-150x150.png" alt="Go编程模式：修饰器" width="150" height="150" /></a><a href="https://coolshell.cn/articles/17929.html" class="wp_rp_title">Go编程模式：修饰器</a></li><li ><a href="https://coolshell.cn/articles/12052.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/29.jpg" alt="Leetcode 编程训练" width="150" height="150" /></a><a href="https://coolshell.cn/articles/12052.html" class="wp_rp_title">Leetcode 编程训练</a></li><li ><a href="https://coolshell.cn/articles/11656.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2014/06/software_development-150x150.png" alt="开发团队的效率" width="150" height="150" /></a><a href="https://coolshell.cn/articles/11656.html" class="wp_rp_title">开发团队的效率</a></li><li ><a href="https://coolshell.cn/articles/11432.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2014/04/code_review-150x150.jpg" alt="从Code Review 谈如何做技术" width="150" height="150" /></a><a href="https://coolshell.cn/articles/11432.html" class="wp_rp_title">从Code Review 谈如何做技术</a></li><li ><a href="https://coolshell.cn/articles/11265.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2014/03/snake-hat-new-year-schedule-800x960-150x150.jpg" alt="Python修饰器的函数式编程" width="150" height="150" /></a><a href="https://coolshell.cn/articles/11265.html" class="wp_rp_title">Python修饰器的函数式编程</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/17757.html">如何重构“箭头型”代码</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/17757.html/feed</wfw:commentRss>
			<slash:comments>56</slash:comments>
		
		
			</item>
		<item>
		<title>Cuckoo Filter：设计与实现</title>
		<link>https://coolshell.cn/articles/17225.html</link>
					<comments>https://coolshell.cn/articles/17225.html#comments</comments>
		
		<dc:creator><![CDATA[Leo]]></dc:creator>
		<pubDate>Wed, 02 Sep 2015 01:18:54 +0000</pubDate>
				<category><![CDATA[C/C++语言]]></category>
		<category><![CDATA[数据库]]></category>
		<category><![CDATA[程序设计]]></category>
		<category><![CDATA[趣味问题]]></category>
		<category><![CDATA[Algorithm]]></category>
		<category><![CDATA[filter]]></category>
		<category><![CDATA[hashing]]></category>
		<category><![CDATA[海量数据]]></category>
		<guid isPermaLink="false">http://coolshell.cn/?p=17225</guid>

					<description><![CDATA[<p>（感谢网友 @我的上铺叫路遥 投稿） 对于海量数据处理业务，我们通常需要一个索引数据结构，用来帮助查询，快速判断数据记录是否存在，这种数据结构通常又叫过滤器(f...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/17225.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/17225.html">Cuckoo Filter：设计与实现</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><strong>（感谢网友 </strong><a href="http://weibo.com/fullofbull" target="_blank"><strong>@我的上铺叫路遥</strong></a><strong> 投稿）</strong></p>
<p><img decoding="async" loading="lazy" class="alignright wp-image-17243 size-medium" src="https://coolshell.cn/wp-content/uploads/2015/08/cuckoo-300x164.jpg" alt="" width="300" height="164" srcset="https://coolshell.cn/wp-content/uploads/2015/08/cuckoo-300x164.jpg 300w, https://coolshell.cn/wp-content/uploads/2015/08/cuckoo.jpg 400w" sizes="(max-width: 300px) 100vw, 300px" /></p>
<p>对于海量数据处理业务，我们通常需要一个索引数据结构，用来帮助查询，快速判断数据记录是否存在，这种数据结构通常又叫过滤器(filter)。考虑这样一个场景，上网的时候需要在浏览器上输入URL，这时浏览器需要去判断这是否一个恶意的网站，它将对本地缓存的成千上万的URL索引进行过滤，如果不存在，就放行，如果（可能）存在，则向远程服务端发起验证请求，并回馈客户端给出警告。</p>
<p>索引的存储又分为有序和无序，前者使用关联式容器，比如B树，后者使用哈希算法。这两类算法各有优劣：比如，关联式容器时间复杂度稳定O(logN)，且支持范围查询；又比如哈希算法的查询、增删都比较快O(1)，但这是在理想状态下的情形，遇到碰撞严重的情况，哈希算法的时间复杂度会退化到O(n)。因此，选择一个好的哈希算法是很重要的。</p>
<p>时下一个非常流行的哈希索引结构就是<strong><a href="https://en.wikipedia.org/wiki/Bloom_filter" target="_blank">bloom filter</a></strong>，它类似于bitmap这样的hashset，所以空间利用率很高。其独特的地方在于它使用多个哈希函数来避免哈希碰撞，如图所示（<a href="https://en.wikipedia.org/wiki/Bloom_filter" target="_blank">来源wikipedia</a>），bit数组初始化为全0，插入x时，x被3个哈希函数分别映射到3个不同的bit位上并置1，查询x时，只有被这3个函数映射到的bit位全部是1才能说明x可能存在，但凡至少出现一个0表示x肯定不存在。</p>
<p><img decoding="async" loading="lazy" class="aligncenter size-full wp-image-17242" src="https://coolshell.cn/wp-content/uploads/2015/08/Bloom_filter.png" alt="Bloom_filter" width="649" height="233" srcset="https://coolshell.cn/wp-content/uploads/2015/08/Bloom_filter.png 649w, https://coolshell.cn/wp-content/uploads/2015/08/Bloom_filter-300x108.png 300w" sizes="(max-width: 649px) 100vw, 649px" /></p>
<p><span id="more-17225"></span></p>
<p>但是，bloom filter的这种位图模式带来两个问题：一个是<strong>误报（false positives）</strong>，在查询时能提供“一定不存在”，但只能提供“可能存在”，因为存在其它元素被映射到部分相同bit位上，导致该位置1，那么一个不存在的元素可能会被误报成存在；另一个是<strong>漏报（false nagatives）</strong>，同样道理，如果删除了某个元素，导致该映射bit位被置0，那么本来存在的元素会被漏报成不存在。由于后者问题严重得多，所以bloom filter必须确保“definitely no”从而容忍“probably yes”，不允许元素的删除。</p>
<p>关于元素删除的问题，一个改良方案是对bloom filter引入计数，但这样一来，原来每个bit空间就要扩张成一个计数值，空间效率上又降低了。</p>
<h4>Cuckoo Hashing</h4>
<p>为了解决这一问题，本文引入了一种新的哈希算法——<strong>cuckoo filter</strong>，它既可以确保该元素存在的必然性，又可以在不违背此前提下删除任意元素，仅仅比bitmap牺牲了微量空间效率。先说明一下，这个算法的思想来源是一篇<a href="http://www.cs.cmu.edu/~binfan/papers/conext14_cuckoofilter.pdf" target="_blank">CMU论文</a>，笔者按照其思路用C语言做了一个简单实现（<a href="https://github.com/begeekmyfriend/CuckooFilter" target="_blank">Github</a>），附上对一段文本数据进行导入导出的正确性测试。</p>
<p>接下来我会结合自己的示例代码讲解哈希算法的实现。我们先来看看cuckoo hashing有什么特点，它的哈希函数是成对的（具体的实现可以根据需求设计），每一个元素都是两个，分别映射到两个位置，一个是记录的位置，另一个是备用位置。这个备用位置是处理碰撞时用的，这就要说到cuckoo这个名词的典故了，中文名叫布谷鸟，这种鸟有一种即狡猾又贪婪的习性，它不肯自己筑巢，而是把蛋下到别的鸟巢里，而且它的幼鸟又会比别的鸟早出生，布谷幼鸟天生有一种残忍的动作，幼鸟会拼命把未出生的其它鸟蛋挤出窝巢，今后以便独享“养父母”的食物。借助生物学上这一典故，cuckoo hashing处理碰撞的方法，就是把原来占用位置的这个元素踢走，不过被踢出去的元素还要比鸟蛋幸运，因为它还有一个备用位置可以安置，如果备用位置上还有人，再把它踢走，如此往复。直到被踢的次数达到一个上限，才确认哈希表已满，并执行rehash操作。如下图所示（<a href="http://codecapsule.com/2013/07/20/cuckoo-hashing/" target="_blank">图片来源</a>）：</p>
<p><a href="http://codecapsule.com/2013/07/20/cuckoo-hashing/"><img decoding="async" loading="lazy" class="aligncenter size-full wp-image-17244" src="https://coolshell.cn/wp-content/uploads/2015/08/cuckoo_preview.jpg" alt="cuckoo_preview" width="720" height="326" srcset="https://coolshell.cn/wp-content/uploads/2015/08/cuckoo_preview.jpg 720w, https://coolshell.cn/wp-content/uploads/2015/08/cuckoo_preview-300x136.jpg 300w" sizes="(max-width: 720px) 100vw, 720px" /></a></p>
<p>&nbsp;</p>
<p>我们不禁要问发生哈希碰撞之前的空间利用率是多少呢？不幸地告诉你，一维数组的哈希表上跟其它哈希函数没什么区别，也就50%而已。但如果是二维的呢？</p>
<p>一个改进的哈希表如下图所示，每个桶（bucket）有4路槽位（slot）。当哈希函数映射到同一个bucket中，在其它三路slot未被填满之前，是不会有元素被踢的，这大大缓冲了碰撞的几率。笔者自己的简单实现上测过，采用二维哈希表（4路slot）大约80%的占用率（CMU论文数据据说达到90%以上，应该是扩大了slot关联数目所致）。</p>
<p><img decoding="async" loading="lazy" class="aligncenter wp-image-17241" src="https://coolshell.cn/wp-content/uploads/2015/08/cuckoo-hashing-1024x392.png" alt="cuckoo hashing" width="650" height="249" srcset="https://coolshell.cn/wp-content/uploads/2015/08/cuckoo-hashing-1024x392.png 1024w, https://coolshell.cn/wp-content/uploads/2015/08/cuckoo-hashing-300x115.png 300w, https://coolshell.cn/wp-content/uploads/2015/08/cuckoo-hashing-900x344.png 900w, https://coolshell.cn/wp-content/uploads/2015/08/cuckoo-hashing.png 1143w" sizes="(max-width: 650px) 100vw, 650px" /></p>
<h4>Cuckoo Filter设计与实现</h4>
<p>cuckoo hashing的原理介绍完了，下面就来演示一下笔者自己实现的一个cuckoo filter应用，简单易用为主，不到500行C代码。应用场景是这样的：假设有一段文本数据，我们把它通过cuckoo filter导入到一个虚拟的flash中，再把它导出到另一个文本文件中。flash存储的单元页面是一个log_entry，里面包含了一对key/value，value就是文本数据，key就是这段大小的数据的SHA1值（照理说SHA1是可以通过数据源生成，没必要存储到flash，但这里主要为了测试而故意设计的，万一key和value之间没有推导关系呢）。</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">
#define SECTOR_SIZE    (1 &lt;&lt; 10)
#define DAT_LEN        (SECTOR_SIZE - 20)  /* minus sha1 size */

/* The log entries store key-value pairs on flash and the
 * size of each entry is assumed just one sector fit.
 */
struct log_entry {
        uint8_t sha1[20];
        uint8_t data[DAT_LEN];
};
</pre>
<p>顺便说明一下DAT_LEN设置，之前我们设计了一个虚拟flash（用malloc模拟出来），由于flash的单位是按页大小SECTOR_SIZE读写，这里假设每个log_entry正好一个页大小，当然可以根据实际情况调整。</p>
<p>以上是flash的存储结构，至于哈希表里的slot有三个成员tag，status和offset，分别是哈希值，状态值和在flash的偏移位置。其中status有三个枚举值：AVAILIBLE，OCCUPIED，DELETED，分别表示这个slot是空闲的，占用的还是被删除的。至于tag，按理说应该有两个哈希值，对应两个哈希函数，但其中一个已经对应bucket的位置上了，所以我们只要保存另一个备用bucket的位置就行了，这样万一被踢，只要用这个tag就可以找到它的另一个安身之所。</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">
enum { AVAILIBLE, OCCUPIED, DELETED, };

/* The in-memory hash bucket cache is to filter keys (which is assumed SHA1) via
 * cuckoo hashing function and map keys to log entries stored on flash.
 */
struct hash_slot_cache {
        uint32_t tag : 30;  /* summary of key */
        uint32_t status : 2;  /* FSM */
        uint32_t offset;  /* offset on flash memory */
};
</pre>
<p>乍看之下size有点大是吗？没关系，你也可以根据情况调整数据类型大小，比如uint16_t，这里仅仅为了测试正确性。</p>
<p>至于哈希表以及bucket和slot的创建见初始化代码。buckets是一个二级指针，每个bucket指向4个slot大小的缓存，即4路slot，那么bucket_num也就是slot_num的1/4。这里我们故意把slot_num调小了点，为的是测试rehash的发生。</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">
#define ASSOC_WAY  (4)  /* 4-way association */

struct hash_table {
    struct hash_slot_cache **buckets;
    struct hash_slot_cache *slots;
    uint32_t slot_num;
    uint32_t bucket_num;
};

int cuckoo_filter_init(size_t size)
{
    ...
    /* Allocate hash slots */
    hash_table.slot_num = nvrom_size / SECTOR_SIZE;
    /* Make rehashing happen */
    hash_table.slot_num /= 4;
    hash_table.slots = calloc(hash_table.slot_num, sizeof(struct hash_slot_cache));
    if (hash_table.slots == NULL) {
        return -1;
    }

    /* Allocate hash buckets associated with slots */
    hash_table.bucket_num = hash_table.slot_num / ASSOC_WAY;
    hash_table.buckets = malloc(hash_table.bucket_num * sizeof(struct hash_slot_cache *));
    if (hash_table.buckets == NULL) {
        free(hash_table.slots);
        return -1;
    }
    for (i = 0; i &lt; hash_table.bucket_num; i++) {
        hash_table.buckets[i] = &amp;hash_table.slots[i * ASSOC_WAY];
    }
}
</pre>
<p>下面是哈希函数的设计，这里有两个，前面提到既然key是20字节的SHA1值，我们就可以分别是对key的低32位和高32位进行位运算，只要bucket_num满足2的幂次方，我们就可以将key的一部分同bucket_num &#8211; 1相与，就可以定位到相应的bucket位置上，注意bucket_num随着rehash而增大，哈希函数简单的好处是求哈希值十分快。</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">
#define cuckoo_hash_lsb(key, count)  (((size_t *)(key))[0] &amp; (count - 1))
#define cuckoo_hash_msb(key, count)  (((size_t *)(key))[1] &amp; (count - 1))
</pre>
<p>终于要讲解cuckoo filter最重要的三个操作了——查询、插入还有删除。查询操作是简单的，我们对传进来的参数key进行两次哈希求值tag[0]和tag[1]，并先用tag[0]定位到bucket的位置，从4路slot中再去对比tag[1]。只有比中了tag后，由于只是key的一部分，我们再去从flash中验证完整的key，并把数据在flash中的偏移值read_addr输出返回。相应的，如果bucket[tag[0]]的4路slot都没有比中，我们再去bucket[tag[1]]中比对（代码略），如果还比不中，可以肯定这个key不存在。<strong>这种设计的好处就是减少了不必要的flash读操作，每次比对的是内存中的tag而不需要完整的key。</strong></p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">static int cuckoo_hash_get(struct hash_table *table, uint8_t *key, uint8_t **read_addr)
{
    int i, j;
    uint8_t *addr;
    uint32_t tag[2], offset;
    struct hash_slot_cache *slot;

    tag[0] = cuckoo_hash_lsb(key, table-&gt;bucket_num);
    tag[1] = cuckoo_hash_msb(key, table-&gt;bucket_num);

    /* Filter the key and verify if it exists. */
    slot = table-&amp;gt;buckets[tag[0]];
    for (i = 0; i bucket_num) == slot[i].tag) {
        if (slot[i].status == OCCUPIED) {
            offset = slot[i].offset;
            addr = key_verify(key, offset);
            if (addr != NULL) {
                if (read_addr != NULL) {
                    *read_addr = addr;
                }
                break;
            }
        } else if (slot[i].status == DELETED) {
            return DELETED;
        }
    }
    ...
}</pre>
<p>接下来先将简单的删除操作，之所以简单是因为delete除了将相应slot的状态值设置一下之外，其实什么都没有干，也就是说它不会真正到flash里面去把数据清除掉。为什么？很简单，没有必要。还有一个原因，flash的写操作之前需要擦除整个页面，这种擦除是会折寿的，<strong>所以很多flash支持随机读，但必须保持顺序写。</strong></p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">static void cuckoo_hash_delete(struct hash_table *table, uint8_t *key)
{
    uint32_t i, j, tag[2];
    struct hash_slot_cache *slot;

    tag[0] = cuckoo_hash_lsb(key, table-&gt;bucket_num);
    tag[1] = cuckoo_hash_msb(key, table-&gt;bucket_num);

    slot = table-&gt;buckets[tag[0]];
    for (i = 0; i bucket_num) == slot[i].tag) {
        slot[i].status = DELETED;
        return;
    }
    ...
}</pre>
<p>了解了flash的读写特性，你就知道为啥插入操作在flash层面要设计成append。不过我们这里不讨论过多flash细节，哈希表层面的插入逻辑其实跟查询差不多，我就不贴代码了。这里要贴的是如何判断并处理碰撞，其实这里也没啥玄机，就是用old_tag和old_offset保存一下临时变量，以便一个元素被踢出去之后还能找到备用的安身之所。但这里会有一个判断，每次踢人都会计数，当alt_cnt大于512时候表示哈希表真的快满了，这时候需要rehash了。</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">static int cuckoo_hash_collide(struct hash_table *table, uint32_t *tag, uint32_t *p_offset)
{
    int i, j, k, alt_cnt;
    uint32_t old_tag[2], offset, old_offset;
    struct hash_slot_cache *slot;

    /* Kick out the old bucket and move it to the alternative bucket. */
    offset = *p_offset;
    slot = table-&gt;buckets[tag[0]];
    old_tag[0] = tag[0];
    old_tag[1] = slot[0].tag;
    old_offset = slot[0].offset;
    slot[0].tag = tag[1];
    slot[0].offset = offset;
    i = 0 ^ 1;
    k = 0;
    alt_cnt = 0;

KICK_OUT:
    slot = table-&gt;buckets[old_tag[i]];
    for (j = 0; j &lt; ASSOC_WAY; j++) {
        if (offset == INVALID_OFFSET &amp;&amp; slot[j].status == DELETED) {
            slot[j].status = OCCUPIED;
            slot[j].tag = old_tag[i ^ 1];
            *p_offset = offset = slot[j].offset;
            break;
        } else if (slot[j].status == AVAILIBLE) {
            slot[j].status = OCCUPIED;
            slot[j].tag = old_tag[i ^ 1];
            slot[j].offset = old_offset;
            break;
        }
    }

    if (j == ASSOC_WAY) {
        if (++alt_cnt &gt; 512) {
            if (k == ASSOC_WAY - 1) {
                /* Hash table is almost full and needs to be resized */
                return 1;
            } else {
                k++;
            }
        }
        uint32_t tmp_tag = slot[k].tag;
        uint32_t tmp_offset = slot[k].offset;
        slot[k].tag = old_tag[i ^ 1];
        slot[k].offset = old_offset;
        old_tag[i ^ 1] = tmp_tag;
        old_offset = tmp_offset;
        i ^= 1;
        goto KICK_OUT;
    }

    return 0;
}</pre>
<p>rehash的逻辑也很简单，无非就是把哈希表中的buckets和slots重新realloc一下，空间扩展一倍，然后再从flash中的key重新插入到新的哈希表里去。这里有个陷阱要注意，<strong>千万不能有相同的key混进来！</strong>虽然cuckoo hashing不像开链法那样会退化成O(n)，但由于每个元素有两个哈希值，而且每次计算的哈希值随着哈希表rehash的规模而不同，相同的key并不能立即检测到冲突，但当相同的key达到一定规模后，噩梦就开始了，由于rehash里面有插入操作，一旦在这里触发碰撞，又会触发rehash，这时就是一个rehash不断递归的过程，由于其中老的内存没释放，新的内存不断重新分配，整个程序就如同陷入DoS攻击一般瘫痪了。<strong>所以每次插入操作前一定要判断一下key是否已经存在过，并且对rehash里的插入使用碰撞断言防止此类情况发生。</strong>笔者在测试中不幸中了这样的彩蛋，调试了大半天才搞清楚原因，搞IT的同学们记住一定要防小人啊~</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">static void cuckoo_rehash(struct hash_table *table)
{
    ...
    uint8_t *read_addr = nvrom_base_addr;
    uint32_t entries = log_entries;
    while (entries--) {
        uint8_t key[20];
        uint32_t offset = read_addr - nvrom_base_addr;
        for (i = 0; i &amp;lt; 20; i++) {
            key[i] = flash_read(read_addr);
            read_addr++;
        }
        /* Duplicated keys in hash table which can cause eternal
         * hashing collision! Be careful of that!
         */
        assert(!cuckoo_hash_put(table, key, &amp;offset));
        if (cuckoo_hash_get(&amp;old_table, key, NULL) == DELETED) {
            cuckoo_hash_delete(table, key);
        }
        read_addr += DAT_LEN;
    }
    ...
}</pre>
<p>到此为止代码的逻辑还是比较简单，使用效果如何呢？我来帮你找个大文件<a href="https://github.com/unqlite/unqlite/blob/master/unqlite.c" target="_blank">unqlite.c</a>测试一下，这是一个嵌入式数据库源代码，共59959行代码。作为需要导入的文件，编译我们的cuckoo filter，然后执行：</p>
<p><code data-enlighter-language="shell" class="EnlighterJSRAW">./cuckoo_db unqlite.c output.c</code></p>
<p>你会发现生成output.c正好也是59959行代码，一分不差，probably yes终于变成了definitely yes。同时也可以看到，cuckoo filter真的很快！如果你想看hashing的整个过程，可以参照<a href="https://github.com/begeekmyfriend/CuckooFilter/blob/master/README.md" target="_blank">README</a>里把调试宏打开。最后，欢迎给<a href="https://github.com/begeekmyfriend/CuckooFilter" target="_blank">这个小玩意</a>提交PR！</p>
<h4>参考资料</h4>
<p>Cuckoo Filter的<a href="http://www.cs.cmu.edu/~binfan/papers/conext14_cuckoofilter.pdf" target="_blank">论文</a>和<a href="http://www.cs.cmu.edu/~binfan/papers/conext14_cuckoofilter.pptx" target="_blank">PPT</a>：Cuckoo Filter: Practically Better Than Bloom<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/12052.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/29.jpg" alt="Leetcode 编程训练" width="150" height="150" /></a><a href="https://coolshell.cn/articles/12052.html" class="wp_rp_title">Leetcode 编程训练</a></li><li ><a href="https://coolshell.cn/articles/11847.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2014/08/puzzle-150x150.png" alt="谜题的答案和活动的心得体会" width="150" height="150" /></a><a href="https://coolshell.cn/articles/11847.html" class="wp_rp_title">谜题的答案和活动的心得体会</a></li><li ><a href="https://coolshell.cn/articles/11832.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2014/08/538efefbgw1eiz9cvx78fj20rm0fmdi8-150x150.jpg" alt="【活动】解迷题送礼物" width="150" height="150" /></a><a href="https://coolshell.cn/articles/11832.html" class="wp_rp_title">【活动】解迷题送礼物</a></li><li ><a href="https://coolshell.cn/articles/10590.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2013/10/QR-Code-Overview-150x150.jpeg" alt="二维码的生成细节和原理" width="150" height="150" /></a><a href="https://coolshell.cn/articles/10590.html" class="wp_rp_title">二维码的生成细节和原理</a></li><li ><a href="https://coolshell.cn/articles/10427.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2013/10/buddy-memory-allocation-150x150.jpg" alt="伙伴分配器的一个极简实现" width="150" height="150" /></a><a href="https://coolshell.cn/articles/10427.html" class="wp_rp_title">伙伴分配器的一个极简实现</a></li><li ><a href="https://coolshell.cn/articles/9886.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/8.jpg" alt="二叉树迭代器算法" width="150" height="150" /></a><a href="https://coolshell.cn/articles/9886.html" class="wp_rp_title">二叉树迭代器算法</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/17225.html">Cuckoo Filter：设计与实现</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/17225.html/feed</wfw:commentRss>
			<slash:comments>37</slash:comments>
		
		
			</item>
		<item>
		<title>vfork 挂掉的一个问题</title>
		<link>https://coolshell.cn/articles/12103.html</link>
					<comments>https://coolshell.cn/articles/12103.html#comments</comments>
		
		<dc:creator><![CDATA[陈皓]]></dc:creator>
		<pubDate>Thu, 20 Nov 2014 16:48:27 +0000</pubDate>
				<category><![CDATA[C/C++语言]]></category>
		<category><![CDATA[Unix/Linux]]></category>
		<category><![CDATA[操作系统]]></category>
		<category><![CDATA[fork]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Unix]]></category>
		<category><![CDATA[vfork]]></category>
		<guid isPermaLink="false">http://coolshell.cn/?p=12103</guid>

					<description><![CDATA[<p>在知乎上，有个人问了这样的一个问题——为什么vfork的子进程里用return，整个程序会挂掉，而且exit()不会？并给出了如下的代码，下面的代码一运行就挂掉...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/12103.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/12103.html">vfork 挂掉的一个问题</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><img decoding="async" loading="lazy" class="alignright wp-image-12105" src="https://coolshell.cn/wp-content/uploads/2014/11/tux-fork-298x300.gif" alt="tux-fork" width="199" height="200" srcset="https://coolshell.cn/wp-content/uploads/2014/11/tux-fork-298x300.gif 298w, https://coolshell.cn/wp-content/uploads/2014/11/tux-fork-150x150.gif 150w, https://coolshell.cn/wp-content/uploads/2014/11/tux-fork-200x200.gif 200w, https://coolshell.cn/wp-content/uploads/2014/11/tux-fork-268x270.gif 268w" sizes="(max-width: 199px) 100vw, 199px" />在知乎上，有个人问了这样的<a href="http://www.zhihu.com/question/26591968" target="_blank">一个问题</a>——为什么vfork的子进程里用return，整个程序会挂掉，而且exit()不会？并给出了如下的代码，下面的代码一运行就挂掉了，但如果把子进程的return改成exit(0)就没事。</p>
<p>我受邀后本来不想回答这个问题的，因为这个问题明显就是RTFM的事，后来，发现这个问题放在那里好长时间，而挂在下面的几个答案又跑偏得比较严重，我觉得可能有些朋友看到那样的答案会被误导，所以就上去回答了一下这个问题。</p>
<p>下面我把问题和我的回答发布在这里，也供更多的人查看。</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;unistd.h&gt;
int main(void) {
    int var;
    var = 88;
    if ((pid = vfork()) &lt; 0) {
        printf(&quot;vfork error&quot;);
        exit(-1);
    } else if (pid == 0) { /* 子进程 */
        var++;
        return 0;
    }
    printf(&quot;pid=%d, glob=%d, var=%d\n&quot;, getpid(), glob, var);
    return 0;
}
</pre>
<p><span id="more-12103"></span></p>
<h4><b>基础知识</b></h4>
<p>首先说一下fork和vfork的差别：</p>
<ul>
<li>fork 是 创建一个子进程，并把父进程的内存数据copy到子进程中。</li>
<li>vfork是 创建一个子进程，并和父进程的内存数据share一起用。</li>
</ul>
<p>这两个的差别是，一个是copy，一个是share。（关于fork，可以参看酷壳之前的《<a title="一个fork的面试题" href="https://coolshell.cn/articles/7965.html" target="_blank">一道fork的面试题</a>》）</p>
<p>你 man vfork 一下，你可以看到，vfork是这样的工作的，</p>
<p style="padding-left: 30px;">1）保证子进程先执行。<br />
2）当子进程调用exit()或exec()后，父进程往下执行。</p>
<p>那么，为什么要干出一个vfork这个玩意？ 原因在man page也讲得很清楚了：</p>
<blockquote><p><strong>Historic Description</strong></p>
<p>Under Linux, fork(2) is implemented using copy-on-write pages, so the only penalty incurred by fork(2) is the time and memory required to duplicate the parent’s page tables, and to create a unique task structure for the child. <b>However, in the bad old days a fork(2) would require making </b><b>a complete copy of the caller’s data space, often needlessly, since usually immediately afterwards an exec(3) is done. Thus, for greater efficiency, BSD introduced the vfork() system call, which did not fully copy the address space of the parent process, but borrowed the parent’s mem</b><b>ory and thread of control until a call to execve(2) or an exit occurred.</b> The parent process was suspended while the child was using its resources. The use of vfork() was tricky: for example, not modifying data in the parent process depended on knowing which variables are held in a register.</p></blockquote>
<p>意思是这样的—— <b>起初只有fork，但是很多程序在fork一个子进程后就exec一个外部程序，于是fork需要copy父进程的数据这个动作就变得毫无意了，而且这样干还很重</b>（注：后来，fork做了优化，详见本文后面）<b>，所以，BSD搞出了个父子进程共享的 vfork，这样成本比较低。因此，vfork本就是为了exec而生。</b></p>
<h4><b>为什么return会挂掉，exit()不会？</b></h4>
<p>从上面我们知道，<b>结束子进程的调用是exit()而不是return，如果你在vfork中return了，那么，这就意味main()函数return了，注意因为函数栈父子进程共享，所以整个程序的栈就跪了。</b></p>
<p>如果你在子进程中return，那么基本是下面的过程：</p>
<p style="padding-left: 30px;"><b>1）子进程的main() 函数 return了，于是程序的函数栈发生了变化。</b></p>
<p style="padding-left: 30px;"><b>2）而main()函数return后，通常会调用 exit()或相似的函数</b>（如：_exit()，exitgroup()）</p>
<p style="padding-left: 30px;"><b>3）这时，父进程收到子进程exit()，开始从vfork返回，但是尼玛，老子的栈都被你子进程给return干废掉了，你让我怎么执行？</b>（注：栈会返回一个诡异一个栈地址，对于某些内核版本的实现，直接报“栈错误”就给跪了，然而，对于某些内核版本的实现，于是有可能会再次调用main()，于是进入了一个无限循环的结果，直到vfork 调用返回 error）</p>
<p>好了，现在再回到 return 和 exit，return会释放局部变量，并弹栈，回到上级函数执行。exit直接退掉。如果你用c++ 你就知道，return会调用局部对象的析构函数，exit不会。（注：exit不是系统调用，是glibc对系统调用 _exit()或_exitgroup()的封装）</p>
<p>可见，<b>子进程调用exit() 没有修改函数栈，所以，父进程得以顺利执行</b>。</p>
<p><strong>但是！注意！如果你调用 exit() 函数，还是会有问题的，正确的方法应该是调用 _exit() 函数，因为 exit() 函数 会 flush 并 close 所有的 标准 I/O ，这样会导致父进程受到影响。（这个情况在fork下也会受到影响，会导致一些被buffer的数据被flush两次，这里可以参看《<a href="https://coolshell.cn/articles/7965.html" target="_blank">一个fork的面试题</a>》）</strong></p>
<h4>关于fork的优化</h4>
<p>很明显，fork太重，而vfork又太危险，所以，就有人开始优化fork这个系统调用。优化的技术用到了著名的<b>写时拷贝（COW）</b>。</p>
<p>也就是说，<strong>对于fork后并不是马上拷贝内存，而是只有你在需要改变的时候，才会从父进程中拷贝到子进程中，这样fork后立马执行exec的成本就非常小了</strong>。所以，Linux的Man Page中并不鼓励使用vfork() ——</p>
<blockquote><p>“ It is rather unfortunate that Linux revived this specter from the past. The BSD man page states: &#8220;This system call will be eliminated when proper system sharing mechanisms are implemented. Users should not depend on the memory sharing semantics of vfork() as it will, in that case, be made synonymous to fork(2).&#8221;”</p></blockquote>
<p>于是，从BSD4.4开始，他们让vfork和fork变成一样的了</p>
<p>但在后来，NetBSD 1.3 又把传统的vfork给捡了回来，说是vfork的性能在 Pentium Pro 200MHz 的机器（这机器好古董啊）上有可以提高几秒钟的性能。详情见——“<a class=" wrap external" href="http://www.netbsd.org/docs/kernel/vfork.html" target="_blank" rel="nofollow noreferrer">NetBSD Documentation: Why implement traditional vfork()<i class="icon-external"></i></a>”</p>
<p>今天的Linux下，fork和vfork还是各是各的，不过，还是建议你不要用vfork，除非你非常关注性能。</p>
<p>（全文完）<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/18360.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2018/05/300x262-150x150.jpg" alt="程序员练级攻略（2018)  与我的专栏" width="150" height="150" /></a><a href="https://coolshell.cn/articles/18360.html" class="wp_rp_title">程序员练级攻略（2018)  与我的专栏</a></li><li ><a href="https://coolshell.cn/articles/17998.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2017/07/systemd-1-150x150.jpeg" alt="Linux PID 1 和 Systemd" width="150" height="150" /></a><a href="https://coolshell.cn/articles/17998.html" class="wp_rp_title">Linux PID 1 和 Systemd</a></li><li ><a href="https://coolshell.cn/articles/11847.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2014/08/puzzle-150x150.png" alt="谜题的答案和活动的心得体会" width="150" height="150" /></a><a href="https://coolshell.cn/articles/11847.html" class="wp_rp_title">谜题的答案和活动的心得体会</a></li><li ><a href="https://coolshell.cn/articles/9104.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2013/02/sed-superman-150x150.png" alt="sed 简明教程" width="150" height="150" /></a><a href="https://coolshell.cn/articles/9104.html" class="wp_rp_title">sed 简明教程</a></li><li ><a href="https://coolshell.cn/articles/9070.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2013/02/awk-150x150.jpg" alt="AWK 简明教程" width="150" height="150" /></a><a href="https://coolshell.cn/articles/9070.html" class="wp_rp_title">AWK 简明教程</a></li><li ><a href="https://coolshell.cn/articles/8883.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2013/01/linux-bash-300x225-150x150.jpg" alt="应该知道的Linux技巧" width="150" height="150" /></a><a href="https://coolshell.cn/articles/8883.html" class="wp_rp_title">应该知道的Linux技巧</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/12103.html">vfork 挂掉的一个问题</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/12103.html/feed</wfw:commentRss>
			<slash:comments>46</slash:comments>
		
		
			</item>
		<item>
		<title>Leetcode 编程训练</title>
		<link>https://coolshell.cn/articles/12052.html</link>
					<comments>https://coolshell.cn/articles/12052.html#comments</comments>
		
		<dc:creator><![CDATA[陈皓]]></dc:creator>
		<pubDate>Thu, 23 Oct 2014 02:51:54 +0000</pubDate>
				<category><![CDATA[C/C++语言]]></category>
		<category><![CDATA[杂项资源]]></category>
		<category><![CDATA[编程语言]]></category>
		<category><![CDATA[Algorithm]]></category>
		<category><![CDATA[C++]]></category>
		<category><![CDATA[Leetcode]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[Programming]]></category>
		<category><![CDATA[程序员]]></category>
		<category><![CDATA[算法]]></category>
		<category><![CDATA[面试]]></category>
		<guid isPermaLink="false">http://coolshell.cn/?p=12052</guid>

					<description><![CDATA[<p>Leetcode这个网站上的题都是一些经典的公司用来面试应聘者的面试题，很多人通过刷这些题来应聘一些喜欢面试算法的公司，比如：Google、微软、Faceboo...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/12052.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/12052.html">Leetcode 编程训练</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><img decoding="async" loading="lazy" class="alignright wp-image-12054 size-full" src="https://coolshell.cn/wp-content/uploads/2014/10/LeetCodeLogo-1.png" alt="LeetCodeLogo (1)" width="121" height="100" />Leetcode这个网站上的题都是一些经典的公司用来面试应聘者的面试题，很多人通过刷这些题来应聘一些喜欢面试算法的公司，比如：Google、微软、Facebook、Amazon之类的这些公司，基本上是应试教育的功利主义。</p>
<p>我做这些题目的不是为了要去应聘这些公司，而是为了锻炼一下自己的算法和编程能力。因为我开始工作的时候基本没有这样的训练算法和编程的网站，除了大学里的“算法和数据结构”里的好些最基础最基础的知识，基本上没有什么训练。所以，当我看到有人在做这些题的时候，我也蠢蠢欲动地想去刷一下。</p>
<p>于是，我花了3-4个月的业余时间，我把<a href="https://oj.leetcode.com/problems/" target="_blank">Leetcode的154道题</a>全部做完了。（这也是最近我没有太多的时间来写博客的原因，你可以看到我之前<a title="谜题的答案和活动的心得体会" href="https://coolshell.cn/articles/11847.html" target="_blank">做的那个活动</a>中有几个算法题来自于Leetcode）有人说我时间太多了，这里声明一下，我基本上都是利用了晚上10点以后的时间来做这些题的。</p>
<p>LeetCode的题大致分成两类：</p>
<p style="padding-left: 30px;"><strong>1）基础算法的知识</strong>。这些题里面有大量的算法题，解这些题都是有套路的，不是用递归（深度优先DFS，广度优先BFS），就是要用动态规划（Dynamic Programming），或是拆半查找（Binary Search），或是回溯（Back tracing），或是分治法（Divide and Conquer），还有大量的对树，数组、链表、字符串和hash表的操作。<strong>通过做这些题能让你对这些最基础的算法的思路有非常扎实的了解和训练</strong>。对我而言，Dynamic Programming 是我的短板，尤其是一些比较复杂的问题，在推导递推公式上总是有思维的缺陷（数学是我的硬伤），通过做了这些题后，我能感到我在DP的思路上有了很大的收获。</p>
<p style="padding-left: 30px;"><strong>2）编程题</strong>。比如：atoi，strstr，add two num，括号匹配，字符串乘法，通配符匹配，文件路径简化，Text Justification，反转单词等等，这些题的Edge Case, Corner Case有很多。这些题需要你想清楚了再干，只要你稍有疏忽，就会有几个case让你痛不欲生，而且一不小心就会让你的代码会写得又臭又长，无法阅读。<strong>通过做这些题，可以非常好的训练你对各种情况的考虑，以及你对程序代码组织的掌控（其实就是其中的状态变量）。</strong>还记得我在《<a title="函数式编程" href="https://coolshell.cn/articles/10822.html" target="_blank">函数式编程</a>》中说的，程序中的状态是你程序变得复杂难维护的直接原因。</p>
<p>我觉得每个程序员都应该花时间和精力做这些题，因为你会从这些题中得到很大的收益。做完这些题后你一定会明白下面几个道理：</p>
<p><span id="more-12052"></span></p>
<p style="padding-left: 30px;"><strong>1）想清楚了再干</strong>。这个观点我以前就在《<a title="多些时间能少写些代码" href="https://coolshell.cn/articles/5686.html" target="_blank">多些时间可以少些代码</a>》说过。如果你拿到题就上去直接写代码的话，你一定会被各种case打回来了。然后呢，你一着急，你就会进入那种我在《<a title="开发团队的效率" href="https://coolshell.cn/articles/11656.html" target="_blank">开发团队的效率</a>》中说的那种毫无效率case by case的开发模式，而你也进入了“平庸模式”。于是你就会出现下图那样的情况。</p>
<figure id="attachment_12053" aria-describedby="caption-attachment-12053" style="width: 440px" class="wp-caption aligncenter"><img decoding="async" loading="lazy" class="size-full wp-image-12053" src="https://coolshell.cn/wp-content/uploads/2014/10/bug_fixing.gif" alt="Case-by-Case Developement" width="440" height="231" /><figcaption id="caption-attachment-12053" class="wp-caption-text">Case-by-Case Development</figcaption></figure>
<p style="padding-left: 30px;"><strong>2) 编程是脑力劳动，急不得</strong>。这个事情在这做这些题的时候你就会发现，要么是脑子转不过来了，要么就是明明就差一点了，但程序怎么都调不对。如果你越着急的话，你就会发现你会离目标越远，而花的时间也会更多。另外，你会发现这些题基本上都是50行代码内就可以搞定的，但是为了这50行以内的代码，你要花好多时间和精力。coding  50行代码在我们的日常工作中分分钟就完成，而Leetcode里的50行代码却没那么简单，也许，用这个你就可以区别什么是码农，什么是程序员了。</p>
<p style="padding-left: 30px;"><strong>3）加班要不得。</strong>因为我总是在晚上10点以后做题，所以，基本上都是在加班状态中工作。这种状态过上两三天，你就会发现，整个大脑已经不转了，而且不但不转，还会犯很多低级错误，很多事情都想不清楚，一个晚上都在和程序的状态控制做搏斗，代码写得越来越乱，越来越没条理。于是这种时候，我都会休息几天，不做题了，然后再做题的时候，就觉得非常地清楚。可见加班 是编程最致命的敌人！</p>
<p>我把我的C++代码放到了Github上，大家也帮我review一下，看看有没有可以改善的。</p>
<p style="text-align: center;"><strong><a href="https://github.com/haoel/leetcode" target="_blank">https://github.com/haoel/leetcode</a></strong></p>
<p>好了，不多说了，<strong>我希望大家有时间都去练练LeetCode，无论是找工作还是对你的编程能力会有非常大的提高</strong>。</p>
<p>&nbsp;</p>
<p>（全文完）<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/8138.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2012/08/250px-Sheldon_Cooper-150x150.jpg" alt="为什么我反对纯算法面试题" width="150" height="150" /></a><a href="https://coolshell.cn/articles/8138.html" class="wp_rp_title">为什么我反对纯算法面试题</a></li><li ><a href="https://coolshell.cn/articles/19612.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2019/07/1920px-Margaret_Hamilton_-_restoration-e1563697198766-1-150x150.jpg" alt="50年前的登月程序和程序员有多硬核" width="150" height="150" /></a><a href="https://coolshell.cn/articles/19612.html" class="wp_rp_title">50年前的登月程序和程序员有多硬核</a></li><li ><a href="https://coolshell.cn/articles/18360.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2018/05/300x262-150x150.jpg" alt="程序员练级攻略（2018)  与我的专栏" width="150" height="150" /></a><a href="https://coolshell.cn/articles/18360.html" class="wp_rp_title">程序员练级攻略（2018)  与我的专栏</a></li><li ><a href="https://coolshell.cn/articles/18024.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2017/07/api-design-300x278-2-150x150.jpg" alt="API设计原则 &#8211; Qt官网的设计实践总结" width="150" height="150" /></a><a href="https://coolshell.cn/articles/18024.html" class="wp_rp_title">API设计原则 &#8211; Qt官网的设计实践总结</a></li><li ><a href="https://coolshell.cn/articles/9543.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2013/04/weibo-150x150.jpg" alt="“C++的数组不支持多态”？" width="150" height="150" /></a><a href="https://coolshell.cn/articles/9543.html" class="wp_rp_title">“C++的数组不支持多态”？</a></li><li ><a href="https://coolshell.cn/articles/8790.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2012/12/choice-150x150.jpg" alt="程序算法与人生选择" width="150" height="150" /></a><a href="https://coolshell.cn/articles/8790.html" class="wp_rp_title">程序算法与人生选择</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/12052.html">Leetcode 编程训练</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/12052.html/feed</wfw:commentRss>
			<slash:comments>96</slash:comments>
		
		
			</item>
		<item>
		<title>State Threads 回调终结者</title>
		<link>https://coolshell.cn/articles/12012.html</link>
					<comments>https://coolshell.cn/articles/12012.html#comments</comments>
		
		<dc:creator><![CDATA[Leo]]></dc:creator>
		<pubDate>Sun, 12 Oct 2014 14:48:57 +0000</pubDate>
				<category><![CDATA[C/C++语言]]></category>
		<category><![CDATA[Unix/Linux]]></category>
		<category><![CDATA[Web开发]]></category>
		<category><![CDATA[程序设计]]></category>
		<category><![CDATA[C++]]></category>
		<category><![CDATA[coroutine]]></category>
		<category><![CDATA[EDSM]]></category>
		<category><![CDATA[IA]]></category>
		<category><![CDATA[process]]></category>
		<category><![CDATA[thread]]></category>
		<category><![CDATA[Web]]></category>
		<category><![CDATA[协程]]></category>
		<guid isPermaLink="false">http://coolshell.cn/?p=12012</guid>

					<description><![CDATA[<p>（感谢网友 @我的上铺叫路遥 投稿） 上回写了篇《一个“蝇量级”C语言协程库》，推荐了一下Protothreads，通过coroutine模拟了用户级别的mul...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/12012.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/12012.html">State Threads 回调终结者</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><strong>（感谢网友 </strong><a href="http://weibo.com/fullofbull" target="_blank"><strong>@我的上铺叫路遥</strong></a><strong> 投稿）</strong></p>
<p>上回写了篇<a title="一个“蝇量级” C 语言协程库" href="https://coolshell.cn/articles/10975.html" target="_blank">《一个“蝇量级”C语言协程库》</a>，推荐了一下<a title="Protothreads" href="http://dunkels.com/adam/pt/" target="_blank">Protothreads</a>，通过coroutine模拟了用户级别的multi-threading模型，虽然本身足够“轻”，杜绝了系统开销，但这个库本身应用场合主要是内存限制的嵌入式领域，提供原生态组件太少，使用限制太多，比如依赖其它调用产生阻塞等。</p>
<p>这回又替大家在开源界淘了个宝，推荐一个轻量级网络应用框架<strong>State Threads</strong>（以下简称ST），总共也就3000行C代码，跟Protothreads不同在于ST针对的就是<strong>高性能可扩展服务器</strong>领域（值得一提的是Protothreads官网<a title="参考链接" href="http://dunkels.com/adam/pt/links.html" target="_blank">参考链接</a>上第一条就是ST的官网）。在其<a title="FAQ" href="http://state-threads.sourceforge.net/docs/faq.html" target="_blank">FAQ</a>页面上一句引用&#8221;Perfection is achieved not when there is nothing more to add, but rather when there is nothing more to take away.&#8221;可以视为开发人员对ST源码质量的自信。</p>
<h4>历史渊源</h4>
<p>首先介绍一下这个库的历史渊源，从代码贡献者来看，ST不是个人作品，而是有着雄厚的商业支持和应用背景，比如服务器领域，在<a href="http://state-threads.sourceforge.net/news.html" target="_blank">这里</a>你可以看到ST曾作为Apache的多核应用模块发布。其诞生最初是由网景（Netscape）公司的MSPR（Netscape Portable Runtime library）项目中剥离出来，后由SGI（Silicon Graphic Inc）还有Yahoo!公司（前者是主力）开发维护的独立线程库。历史版本方面，作为<a title="SourceForge" href="http://sourceforge.net/projects/state-threads/files/" target="_blank">SourceForge</a>上开源项目，由2001年发布v1.0以来一直到2009年v1.9稳定版后未再变动。在平台移植方面，从Makefile的配置选项中可知ST支持多种Unix-like平台，还有专门针对Win32的源码改写。源码例子中，提供了web server、proxy以及dns三种编程实例供参考。可以说代码质量应该是相当的稳定和可靠的。</p>
<p><span id="more-12012"></span></p>
<p>至于许可证方面，有必要略作说明。出于历史原因，网景最初发布时选择了MPL1.1许可证，而后SGI在维护中又混进了GPLv2许可证，照理说这两种许可证是互不兼容的（MPL1.1后续版本是GPL兼容的），也就是说用双许可证打包发布理论上是非法无效的，见GNU官网上<a title="GPL兼容" href="http://www.gnu.org/licenses/license-list.html#MPL" target="_blank">MPL兼容性</a>一节。但这里有值得商榷的地方，因为文中又提及，根据MPL1.1中某条款第13节，如果整段或部分代码允许采用另一许可证作为备用（alternate）选择，比如GPL及其兼容，那么整个库的许可证就可视为GPL兼容的。如此一来所谓GPL兼容性一般解释为你不能在GPLv2的代码中混入MPL1.1，而不是说你不能在MPL1.1代码中混入GPLv2，也就是说GPLv2在MPL1.1之后是可以接受的，事实上SGI就采用了后面的做法，尚未引起版权上的纠纷。为此我还考证了一下FAQ上<a title="license" href="http://state-threads.sourceforge.net/docs/faq.html" target="_blank">license</a>一节的说法，说ST既可以在MPL和GPL之间选择一种，也可以继续用双许可证，还补了一句在non-free项目使用上也没有限制，但对ST源码所做改动必须对用户可见。在源码文件中的SGI的附加声明还解释了将ST转为GPL代码的做法，就是可以删除前面MPL的声明，否则后续用户仍可以在两者之间二选一。个人觉得既然SGI都这样发话了，那么可解释为反之删除GPL的声明继续采用MPL也是可以接受的，如果你对双许可证承诺仍不放心的话。</p>
<h4>基于事件驱动状态机（EDSM）</h4>
<p>好了，下面该进入技术性话题了。前面说了ST的目标是<strong>高性能可扩展</strong>，其技术特征一言以蔽之就是</p>
<blockquote><p><strong>&#8220;It combines the simplicity of the multi-threaded programming paradigm, in which one thread supports each simultaneous connection, with the performance and scalability of an event-driven state machine (EDSM) architecture.&#8221;</strong></p></blockquote>
<p>我们先来纵向比较ST与传统的EDSM区别，再来横向比较与其它线程库（比如Pthread）的区别（注：以下图片全部来自<a title="ST FAQ" href="http://state-threads.sourceforge.net/docs/faq.html" target="_blank">State Threads Library FAQ</a>）。</p>
<p>传统EDSM最常见的方式就是I/O事件的<strong>异步回调</strong>。基本上都会有一个叫做dispatcher的单线程主循环（又叫event loop），用户通过向dispatcher注册回调函数（又叫event handler）来实现异步通知，从而不必在原地空耗资源干等，在dispatcher主循环中通过select()/poll()系统调用来等待各种I/O事件的发生，当内核检测到事件触发并且数据可达或可用时，select()/poll()会返回从而使dispatcher调用相应的回调函数来对处理用户的请求。所以异步回调与其说是通知，不如说用委托更恰当。</p>
<p>整个过程都是单线程的。<strong>这种处理本质上就是将一堆互不相交（disjoint）的回调实现同步控制，就像串联在一个顺序链表上。</strong>见图1，黑色的双箭头表示I/O事件复用，回调是个筐，里面装着对各种请求的处理（当然不是每个请求都有回调，一个请求也可以对应不同的回调），每个回调被串联起来由dispatcher激活。这里请求等价于thread的概念（不是操作系统的线程），只不过“上下文切换”（context switch）发生在每个回调结束之时（假设不同请求对应不同回调），注册下一个回调以待事件触发时恢复其它请求的处理。至于dispatcher的执行状态（execute state）可作为回调函数的参数保存和传递。</p>
<p><img decoding="async" class="aligncenter" src="https://coolshell.cn/wp-content/uploads/2014/10/edsm.gif" alt="EDSM" /></p>
<p>异步回调的缺陷在于<strong>难以实现和扩展</strong>，虽然已经有libevent这样的通用库，以及其它actor/reacotor的设计模式及其框架，但正如Dean Gaudet（Apache开发者）所说：“其内在的复杂性——<strong>将线性思维分解成一堆回调的负担</strong>（breaking up linear thought into a bucketload of callbacks）——仍然存在”。从上图可见，<strong>回调之间请求例程不是连续的，比如回调之间的切换会打断部分请求，又比如有新的请求需要重新注册。</strong></p>
<p><strong>ST本质上仍然是基于EDSM模型，但旨在取代传统的异步回调方式。</strong>ST将请求抽象为thread概念以更接近自然编程模式（所谓的linear thought吧，就像操作系统的线程之间切换那样自然）。ST的调度器（scheduler）对于用户来说是透明的，不像dispatcher那种将执行状态（execute state）暴露给回调方式。每个thread的现场环境可以保存在栈上（一段连续的大小确定的内存空间），由C的运行环境管理。从图2看到，<strong>ST的threads可以并发地线性地处理I/O事件，模型比异步回调简单得多。</strong></p>
<p><img decoding="async" class="aligncenter" src="https://coolshell.cn/wp-content/uploads/2014/10/st_edsm.gif" alt="State Threads" /></p>
<p>这里稍微解释一下ST调度工作原理，ST运行环境维护了四种队列，分别是IOQ、RUNQ、SLEEPQ以及ZOMBIEQ，<strong>当每个thread处于不同队列中对应不同的状态（ST顾名思义所谓thread状态机）。</strong>比如polling请求的时候，当前thread就加入IOQ表示等待事件（如果有timeout同时会被放到SLEEPQ中），当事件触发时，thread就从IOQ（如果有timeout同时会从SLEEPQ）移除并转移到RUNQ等待被调度，成为当前的running thread，相当于操作系统的就绪队列，跟传统EDSM对应起来就是注册回调以及激活回调。再比如模拟同步控制wait/sleep/lock的时候，当前thread会被放入SLEEPQ，直到被唤醒或者超时再次进入RUNQ以待调度。</p>
<p><strong>ST的调度具备性能与内存双重优点</strong>：在性能上，ST实现自己的setjmp/longjmp来模拟调度，无任何系统开销，并且context（就是jmp_buf）针对不同平台和架构用底层语言实现的，可移植性媲美libc。下面放一段代码解释一下调度实现：</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">/*
 * Switch away from the current thread context by saving its state 
 * and calling the thread scheduler
 */
#define _ST_SWITCH_CONTEXT(_thread)       \
    ST_BEGIN_MACRO                        \
    if (!MD_SETJMP((_thread)-&gt;context)) { \
      _st_vp_schedule();                  \
    }                                     \
    ST_END_MACRO

/*
 * Restore a thread context that was saved by _ST_SWITCH_CONTEXT 
 * or initialized by _ST_INIT_CONTEXT
 */
#define _ST_RESTORE_CONTEXT(_thread)   \
    ST_BEGIN_MACRO                     \
    _ST_SET_CURRENT_THREAD(_thread);   \
    MD_LONGJMP((_thread)-&gt;context, 1); \
    ST_END_MACRO

void _st_vp_schedule(void)
{
    _st_thread_t *thread;

    if (_ST_RUNQ.next != &amp;_ST_RUNQ) {
        /* Pull thread off of the run queue */
        thread = _ST_THREAD_PTR(_ST_RUNQ.next);
        _ST_DEL_RUNQ(thread);
    } else {
        /* If there are no threads to run, switch to the idle thread */
        thread = _st_this_vp.idle_thread;
    }
    ST_ASSERT(thread-&gt;state == _ST_ST_RUNNABLE);

    /* Resume the thread */
    thread-&gt;state = _ST_ST_RUNNING;
    _ST_RESTORE_CONTEXT(thread);
}
</pre>
<p>如果你熟悉setjmp/longjmp的用法，你就知道当前thread在调用MD_SETJMP将现场上下文保存在jmp_buf中并返回返回0，然后自己调用_st_vp_schedule()将自己调度出去。调度器先从RUNQ上找，如果队列为空就找idle thread，这是在整个ST初始化时创建的一个特殊thread，然后将当前线程设为自己，再调用MD_LONGJMP切换到其上次调用MD_SETJMP的地方，从thread-&gt;context恢复现场并返回1，该thread就接着往下执行了。<strong>整个过程就同EDSM一样发生在操作系统单线程下，所以没有任何系统开销与阻塞。</strong></p>
<p><strong>其实真正的阻塞是发生在等待I/O事件复用上，也就是select()/poll()，这是整个ST唯一的系统调用。</strong>ST当前的状态是，整个环境处于空闲状态，所有threads的请求处理都已经完成，也就是RUNQ为空。这时在_st_idle_thread_start维护了一个主循环（类似于event loop），主要负责三种任务：1.对IOQ所有thread进行I/O复用检测；2.对SLEEPQ进行超时检查；3.将idle thread调度出去，代码如下：</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">
void *_st_idle_thread_start(void *arg)
{
    _st_thread_t *me = _ST_CURRENT_THREAD();

    while (_st_active_count &gt; 0) {
        /* Idle vp till I/O is ready or the smallest timeout expired */
        _ST_VP_IDLE();

        /* Check sleep queue for expired threads */
        _st_vp_check_clock();

        me-&gt;state = _ST_ST_RUNNABLE;
        _ST_SWITCH_CONTEXT(me);
    }

    /* No more threads */
    exit(0);

    /* NOTREACHED */
    return NULL;
}</pre>
<p>这里的me就是idle thread，因为_st_idle_thread_start就是创建idle thread的启动点，每从上次_ST_SWITCH_CONTEXT()切换回来的时候，接着在_ST_VP_IDLE()里轮询I/O事件的发生，一旦检测到发生了别的thread事件或者SLEEPQ里面发生超时，再用_ST_SWITCH_CONTEXT()把自己切换出去，如果此时RUNQ中非空的话就切换到队列第一个thread。这里主循环是不会退出的。</p>
<p>在内存方面，<strong>ST的执行状态作为局部变量保存在栈上，而不是像回调需要动态分配，</strong>用户可能分别这样使用thread模式和callback模式：</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">/* thread land */
int foo()
{
    int local1;
    int local2;
    do_some_io();
}

/* callback land */
struct foo_data {
    int local1;
    int local2;
};

void foo_cb(void *arg)
{
    struct foo_data *locals = arg;
    ...
}

void foo()
{
    struct foo_data *locals = malloc(sizeof(struct foo_data));
    register(foo_cb, locals);
}
</pre>
<h4>基于Mult-Threading范式</h4>
<p>同样基于multi-threading编程范式，ST同其它线程库又有和有点呢？比如Posix Thread（以下简称PThread）是个通用的线程库，它是<strong>将用户级线程（thread）同内核执行对象（kernel execution entity，有些书又叫lightweight processes）做了1:1或m:n映射，</strong>从而实现multi-threading模式。<strong>而ST是单线程（n:1映射），它的thread实际上就是协程（coroutine）。</strong>通常的网络应用上，多线程范式绕不开操作系统，但在某些特定的服务器领域，线程间的共享资源会带来额外复杂度，锁、竞态、并发、文件句柄、全局变量、管道、信号等，面对这些Pthread的灵活性会大打折扣。<strong>而ST的调度是精确的，它只会在明确的I/O和同步函数调用点上发生上下文切换，这正是协程的特性，如此一来ST就不需要互斥保护了，进而也可以放心使用任何静态变量和不可重入库函数了</strong>（这在同样作为协程的Protothreads里是不允许的，因为那是stack-less的，无法保存上下文），极大的简化了编程和调试同时增加了性能。</p>
<p>对于同样用户级线程如GNU Pth和MIT Phread比起来呢？有两点，一是ST的thread是<strong>无优先级的非抢占式调度</strong>，也就是说ST基于EDSM的，每个thread都是事件或数据驱动，迟早会把自己调度出去，而且调度点是明确的，并非按时间片来的，从而简化了thread管理；二是ST会<strong>忽略所有信号处理</strong>，在_st_io_init中会把sigact.sa_handler设为SIG_IGN，这样做是因为将thread资源最小化，避免了signal mask及其系统调用（在ucontext上是避免不了的）。但这并不意味着ST就不能处理信号，实际上ST建议将信号写入pipe的方式转化为普通I/O事件处理，示例详见<a title="signal handling" href="http://state-threads.sourceforge.net/docs/notes.html#signals" target="_blank">这里</a>。</p>
<p>这里顺便说一句，<strong>C语言实现的协程据我所知只有三种方式</strong>：Protothread为代表利用switch-case语义跳转，以ST为代表不依赖libc的setjmp/longjmp上下文切换，以及依赖glibc的ucontext接口（<a title="云风的coroutine" href="https://github.com/cloudwu/coroutine" target="_blank">云风的coroutine</a>）。第一种最轻，但受限最大，第三种耗资源性能慢（陈皓注：glibc的ucontext接口的实现中有一个和信号有关的系统调用，所以会慢，估计在一些情况下会比pthread还慢），目前看来ST是最好使的。</p>
<h4>基于多核环境</h4>
<p>下面来聊聊ST在多核环境下的应用。服务器领域多核的优势在于实现了物理上真正的并发，所以如何充分利用系统优势也是线程库的一大难点。这对ST来说也许正是它的拿手好戏，前面提及ST曾作为Apache的多核引擎模块发布。这里要补充一下前面漏掉的ST的一个重要概念——<strong>虚拟处理器</strong>（virtual processor，简称vp），见图3，多个cpu通过内核的SMP模拟出多个“核”（core），一个core对应一个内核任务（kernel task），同时对应一个用户进程（process），一个process对应ST的一个vp，每个vp下就是ST的thread（是协程不是线程），结合前面所述，vp初始化先创建idle thread，然后根据I/O事件驱动其它threads，这就是ST的多核架构。</p>
<p><img decoding="async" class="aligncenter" src="https://coolshell.cn/wp-content/uploads/2014/10/st_app.gif" alt="multi-core" /></p>
<p>这里要指出的是，<strong>ST只负责自身thread调度，进程管理是应用程序的事情，</strong>也就是说由用户来决定fork多少进程，每个进程分配多少资源，如何进行IPC等。这种架构的好处就是每个vp有自己独立的空间，避免了资源同步竞态（比如杜绝了多进程里的多线程这样混乱的模型）。我们知道这种<strong>基于进程的架构是非常健壮的，一个进程奔溃不会影响到其它进程，同时充分利用多核硬件的高并发。</strong>同时对于具体逻辑业务使用vp里的thread处理，这是基于EDSM的，如此一来做到了<strong>逻辑业务与内核执行对象之间的解耦</strong>，没必要因为1K个连接去创建1K的进程。这就是ST的扩展性和灵活性。</p>
<h4>使用限制</h4>
<p>ST的主要限制在于，应用程序所有I/O操作必须使用ST提供的API，因为只有这样thread才能被调度器管理，并且避免阻塞。</p>
<p>另一个限制在于thread调试，这本身不容易，好在v1.9的ST提供了DEBUG参数，使用TREADQ以及_st_iterate_threads接口检测thread调度情况，用户还可自定义_st_show_thread_stack接口dump每个thread的栈，在GDB使能_st_iterate_threads_flag变量，这些都在Readme中对调试方法有具体说明。按下不表。</p>
<h4>总结</h4>
<p>这篇文章写得有点短了，主要是通过对比来介绍ST的，其实还有大段原理可以讲，大段源码以及实战用例可以贴，但这一下子又写不过来，ST还是有点技术含量的。说白了，<strong>ST的核心思想就是利用multi-threading的简单优雅范式胜过传统异步回调的复杂晦涩实现，又利用EDSM的性能和解耦架构避免了multi-threading在系统上的开销和暗礁。</strong>学习ST告诉我们一个道理：<strong>未来技术的趋势永远都是融合的。</strong></p>
<h4>参考</h4>
<ul>
<li>在<a title="sourceforge源码" href="http://sourceforge.net/projects/state-threads/files/" target="_blank">SourceForge</a>以及<a title="github源码" href="https://github.com/winlinvip/state-threads" target="_blank">github</a>上的源码：前者有历史版本及win32版本，后者只有v1.9。</li>
</ul>
<ul>
<li><a title="State Threads for Internet Applications" href="http://state-threads.sourceforge.net/docs/st.html" target="_blank">State Threads for Internet Applications</a>：介绍原理的，值得一看，<a title="中文翻译" href="http://blog.csdn.net/win_lin/article/details/8242653" target="_blank">这里</a>有篇中文翻译附加单元测试（在单CPU 512M内存上创建数万个thread，CPU占用率约5%，内存约4.3K/thread）。</li>
</ul>
<ul>
<li><a title="State Threads Library FAQ" href="http://state-threads.sourceforge.net/docs/faq.html" target="_blank">State Threads Library FAQ</a>：本文基于此而写。</li>
</ul>
<ul>
<li><a title="API手册" href="http://state-threads.sourceforge.net/docs/reference.html" target="_blank">Complete reference</a>：API完全手册。</li>
</ul>
<ul>
<li><a title="注意事项" href="http://state-threads.sourceforge.net/docs/notes.html" target="_blank">Programing Notes</a>：编程注意事项，包括信号处理，IPC，非网络I/O事件等。</li>
</ul>
<p>（全文完）<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="http://coolshell.cn/articles/10975.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/19.jpg" alt="一个“蝇量级” C 语言协程库" width="150" height="150" /></a><a href="http://coolshell.cn/articles/10975.html" class="wp_rp_title">一个“蝇量级” C 语言协程库</a></li><li ><a href="http://coolshell.cn/articles/8711.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2012/12/200906020837401710-150x150.jpg" alt="程序员疫苗：代码注入" width="150" height="150" /></a><a href="http://coolshell.cn/articles/8711.html" class="wp_rp_title">程序员疫苗：代码注入</a></li><li ><a href="http://coolshell.cn/articles/5987.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/7.jpg" alt="如何设计“找回用户帐号”功能" width="150" height="150" /></a><a href="http://coolshell.cn/articles/5987.html" class="wp_rp_title">如何设计“找回用户帐号”功能</a></li><li ><a href="http://coolshell.cn/articles/8309.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/10.jpg" alt="C/C++语言中闭包的探究及比较" width="150" height="150" /></a><a href="http://coolshell.cn/articles/8309.html" class="wp_rp_title">C/C++语言中闭包的探究及比较</a></li><li ><a href="http://coolshell.cn/articles/8239.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2012/09/lock_free_bicycle-150x150.jpg" alt="无锁队列的实现" width="150" height="150" /></a><a href="http://coolshell.cn/articles/8239.html" class="wp_rp_title">无锁队列的实现</a></li><li ><a href="http://coolshell.cn/articles/7965.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2012/07/fork01jpg-150x150.jpg" alt="一个fork的面试题" width="150" height="150" /></a><a href="http://coolshell.cn/articles/7965.html" class="wp_rp_title">一个fork的面试题</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/12012.html">State Threads 回调终结者</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/12012.html/feed</wfw:commentRss>
			<slash:comments>50</slash:comments>
		
		
			</item>
		<item>
		<title>C语言的整型溢出问题</title>
		<link>https://coolshell.cn/articles/11466.html</link>
					<comments>https://coolshell.cn/articles/11466.html#comments</comments>
		
		<dc:creator><![CDATA[陈皓]]></dc:creator>
		<pubDate>Mon, 21 Apr 2014 00:18:01 +0000</pubDate>
				<category><![CDATA[C/C++语言]]></category>
		<category><![CDATA[程序设计]]></category>
		<category><![CDATA[编程语言]]></category>
		<category><![CDATA[网络安全]]></category>
		<category><![CDATA[C++]]></category>
		<category><![CDATA[Overflow]]></category>
		<category><![CDATA[安全]]></category>
		<guid isPermaLink="false">http://coolshell.cn/?p=11466</guid>

					<description><![CDATA[<p>整型溢出有点老生常谈了，bla, bla, bla&#8230; 但似乎没有引起多少人的重视。整型溢出会有可能导致缓冲区溢出，缓冲区溢出会导致各种黑客攻击，比如...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/11466.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/11466.html">C语言的整型溢出问题</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script>整型溢出有点老生常谈了，bla, bla, bla&#8230; 但似乎没有引起多少人的重视。整型溢出会有可能导致缓冲区溢出，缓冲区溢出会导致各种黑客攻击，比如最近OpenSSL的heartbleed事件，就是一个buffer overread的事件。在这里写下这篇文章，希望大家都了解一下整型溢出，编译器的行为，以及如何防范，以写出更安全的代码。</p>
<h4>什么是整型溢出</h4>
<p>C语言的整型问题相信大家并不陌生了。对于整型溢出，分为无符号整型溢出和有符号整型溢出。</p>
<p><strong>对于unsigned整型溢出，C的规范是有定义的</strong>——“溢出后的数会以2^(8*sizeof(type))作模运算”，也就是说，如果一个unsigned char（1字符，8bits）溢出了，会把溢出的值与256求模。例如：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="c">unsigned char x = 0xff;
printf("%d\n", ++x);</pre>
<p>上面的代码会输出：0 （因为0xff + 1是256，与2^8求模后就是0）</p>
<p><strong>对于signed整型的溢出，C的规范定义是“undefined behavior”</strong>，也就是说，编译器爱怎么实现就怎么实现。对于大多数编译器来说，算得啥就是啥。比如：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="c">signed char x =0x7f; //注：0xff就是-1了，因为最高位是1也就是负数了
printf("%d\n", ++x);</pre>
<p>上面的代码会输出：-128，因为0x7f + 0x01得到0x80，也就是二进制的1000 0000，符号位为1，负数，后面为全0，就是负的最小数，即-128。</p>
<p><span id="more-11466"></span></p>
<p>另外，千万别以为signed整型溢出就是负数，这个是不定的。比如：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="c">signed char x = 0x7f;
signed char y = 0x05;
signed char r = x * y;
printf("%d\n", r);</pre>
<p>上面的代码会输出：123</p>
<p>相信对于这些大家不会陌生了。</p>
<h4>整型溢出的危害</h4>
<p>下面说一下，整型溢出的危害。</p>
<h5>示例一：整形溢出导致死循环</h5>
<pre class="EnlighterJSRAW" data-enlighter-language="c"> ... ...
... ...
short len = 0;
... ...
while(len&lt; MAX_LEN) {
    len += readFromInput(fd, buf);
    buf += len;
}</pre>
<p>上面这段代码可能是很多程序员都喜欢写的代码（我在很多代码里看到过多次），其中的MAX_LEN 可能会是个比较大的整型，比如32767，我们知道short是16bits，取值范围是-32768 到 32767 之间。但是，上面的while循环代码有可能会造成整型溢出，而len又是个有符号的整型，所以可能会成负数，导致不断地死循环。</p>
<h5>示例二：整形转型时的溢出</h5>
<pre class="EnlighterJSRAW" data-enlighter-language="c">int copy_something(char *buf, int len)
{
    #define MAX_LEN 256
    char mybuf[MAX_LEN];
     ... ...
     ... ...

     if(len &gt; MAX_LEN){ // &lt;---- [1]
         return -1;
     }

     return memcpy(mybuf, buf, len);
}</pre>
<p>上面这个例子中，还是[1]处的if语句，看上去没有会问题，但是len是个signed int，而memcpy则需一个size_t的len，也就是一个unsigned 类型。于是，len会被提升为unsigned，此时，如果我们给len传一个负数，会通过了if的检查，但在memcpy里会被提升为一个正数，于是我们的mybuf就是overflow了。这个会导致mybuf缓冲区后面的数据被重写。</p>
<h5>示例三：分配内存</h5>
<p>关于整数溢出导致堆溢出的很典型的例子是，OpenSSH Challenge-Response SKEY/BSD_AUTH 远程缓冲区溢出漏洞。下面这段有问题的代码摘自OpenSSH的代码中的auth2-chall.c中的input_userauth_info_response() 函数:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="c">nresp = packet_get_int();
if (nresp &gt; 0) {
    response = xmalloc(nresp*sizeof(char*));
    for (i = 0; i &lt; nresp; i++)
        response[i] = packet_get_string(NULL);
}</pre>
<p>上面这个代码中，nresp是size_t类型（size_t一般就是unsigned int/long int），这个示例是一个解数据包的示例，一般来说，数据包中都会有一个len，然后后面是data。如果我们精心准备一个len，比如：1073741825（在32位系统上，指针占4个字节，unsigned int的最大值是0xffffffff，我们只要提供0xffffffff/4 的值——0x40000000，这里我们设置了0x4000000 + 1）， nresp就会读到这个值，然后nresp<em>sizeof(char</em>)就成了 1073741825 * 4，于是溢出，结果成为了 0x100000004，然后求模，得到4。于是，malloc(4)，于是后面的for循环1073741825 次，就可以干环事了（经过0x40000001的循环,用户的数据早已覆盖了xmalloc原先分配的4字节的空间以及后面的数据，包括程序代码，函数指针，于是就可以改写程序逻辑。关于更多的东西，你可以看一下这篇文章《<a href="http://engj.org/index.php/ej/article/view/112/167" target="_blank" rel="noopener noreferrer">Survey of Protections from Buffer-Overflow Attacks</a>》）。</p>
<h5>示例四：缓冲区溢出导致安全问题</h5>
<pre class="EnlighterJSRAW" data-enlighter-language="c">int func(char *buf1, unsigned int len1,
         char *buf2, unsigned int len2 )
{
   char mybuf[256]; 

   if((len1 + len2) &gt; 256){    //&lt;--- [1]
       return -1;
   } 

   memcpy(mybuf, buf1, len1);
   memcpy(mybuf + len1, buf2, len2); 

   do_some_stuff(mybuf); 

   return 0;
}</pre>
<p>上面这个例子本来是想把buf1和buf2的内容copy到mybuf里，其中怕len1 + len2超过256 还做了判断，但是，如果len1+len2溢出了，根据unsigned的特性，其会与2^32求模，所以，基本上来说，上面代码中的[1]处有可能为假的。（注：通常来说，在这种情况下，如果你开启-O代码优化选项，那个if语句块就全部被和谐掉了——被编译器给删除了）比如，你可以测试一下 len1=0x104， len2 = 0xfffffffc 的情况。</p>
<h5>示例五：size_t 的溢出</h5>
<pre class="EnlighterJSRAW" data-enlighter-language="c">for (int i= strlen(s)-1;  i&gt;=0; i--)  { ... }</pre>
<pre class="EnlighterJSRAW" data-enlighter-language="c">for (int i=v.size()-1; i&gt;=0; i--)  { ... }</pre>
<p>上面这两个示例是我们经常用的从尾部遍历一个数组的for循环。第一个是字符串，第二个是C++中的vector容器。strlen()和vector::size()返回的都是 size_t，size_t在32位系统下就是一个unsigned int。你想想，如果strlen(s)和v.size() 都是0呢？这个循环会成为个什么情况？于是strlen(s) &#8211; 1 和 v.size() &#8211; 1 都不会成为 -1，而是成为了 (unsigned int)(-1)，一个正的最大数。导致你的程序越界访问。</p>
<p>这样的例子有很多很多，这些整型溢出的问题如果在关键的地方，尤其是在搭配有用户输入的地方，如果被黑客利用了，就会导致很严重的安全问题。</p>
<h4>关于编译器的行为</h4>
<p>在谈一下如何正确的检查整型溢出之前，我们还要来学习一下编译器的一些东西。请别怪我罗嗦。</p>
<h5>编译器优化</h5>
<p>如何检查整型溢出或是整型变量是否合法有时候是一件很麻烦的事情，就像上面的第四个例子一样，编译的优化参数-O/-O2/-O3基本上会假设你的程序不会有整形溢出。会把你的代码中检查溢出的代码给优化掉。</p>
<p>关于编译器的优化，在这里再举个例子，假设我们有下面的代码（又是一个相当相当常见的代码）：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="c">int len;
char* data;

if (data + len &lt; data){
    printf("invalid len\n");
    exit(-1);
}
</pre>
<p>上面这段代码中，len 和 data 配套使用，我们害怕len的值是非法的，或是len溢出了，于是我们写下了if语句来检查。这段代码在-O的参数下正常。但是在-O2的编译选项下，整个if语句块被优化掉了。</p>
<p>你可以写个小程序，在gcc下编译（我的版本是4.4.7，记得加上-O2和-g参数），然后用gdb调试时，用disass /m命信输出汇编，你会看到下面的结果（你可以看到整个if语句块没有任何的汇编代码——直接被编译器和谐掉了）：</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">7 int len = 10;
8 char* data = (char *)malloc(len);
0x00000000004004d4 &lt;+4&gt;: mov $0xa,%edi
0x00000000004004d9 &lt;+9&gt;: callq 0x4003b8 &lt;malloc@plt&gt;

9
10 if (data + len &lt; data){
11 printf(&quot;invalid len\n&quot;);
12 exit(-1);
13 }
14
15 }
0x00000000004004de &lt;+14&gt;: add $0x8,%rsp
0x00000000004004e2 &lt;+18&gt;: retq
</pre>
<p>对此，你需要把上面 char* 转型成 uintptr_t 或是 size_t，说白了也就是把char*转成unsigned的数据结构，if语句块就无法被优化了。如下所示：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="c">if ((uintptr_t)data + len &lt; (uintptr_t)data){
    ... ...
}</pre>
<p>关于这个事，你可以看一下C99的规范说明《 <a href="http://www.open-std.org/JTC1/SC22/WG14/www/docs/n1124.pdf" target="_blank" rel="noopener noreferrer">ISO/IEC 9899:1999 C specification</a> 》第 §6.5.6 页，第8点，我截个图如下：（这段话的意思是定义了指针+/-一个整型的行为，如果越界了，则行为是undefined）</p>
<p><img decoding="async" loading="lazy" class="aligncenter size-full wp-image-11469" src="https://coolshell.cn/wp-content/uploads/2014/04/c99.jpg" alt="" width="647" height="310" srcset="https://coolshell.cn/wp-content/uploads/2014/04/c99.jpg 647w, https://coolshell.cn/wp-content/uploads/2014/04/c99-300x144.jpg 300w, https://coolshell.cn/wp-content/uploads/2014/04/c99-564x270.jpg 564w" sizes="(max-width: 647px) 100vw, 647px" /></p>
<p>注意上面标红线的地方，说如果指针指在数组范围内没事，如果越界了就是undefined，也就是说这事交给编译器实现了，编译器想咋干咋干，那怕你想把其优化掉也可以。在这里要重点说一下，<strong>C语言中的一个大恶魔—— Undefined! 这里都是“野兽出没”的地方，你一定要小心小心再小心</strong>。</p>
<h5>花絮：编译器的彩蛋</h5>
<p>上面说了所谓的undefined行为就全权交给编译器实现，gcc在1.17版本下对于undefined的行为还玩了个彩蛋（<a href="http://en.wikipedia.org/wiki/Undefined_behavior#Compiler_easter_eggs" target="_blank" rel="noopener noreferrer">参看Wikipedia</a>）。</p>
<p>下面gcc 1.17版本下的遭遇undefined行为时，gcc在unix发行版下玩的彩蛋的源代码。我们可以看到，它会去尝试去执行一些游戏<a href="http://en.wikipedia.org/wiki/NetHack">NetHack</a>， <a href="http://en.wikipedia.org/wiki/Rogue_%28computer_game%29">Rogue</a> 或是Emacs的 <a href="http://en.wikipedia.org/wiki/Tower_of_Hanoi#Applications">Towers of Hanoi</a>，如果找不到，就输出一条NB的报错。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="c">execl("/usr/games/hack", "#pragma", 0); // try to run the game NetHack
execl("/usr/games/rogue", "#pragma", 0); // try to run the game Rogue
// try to run the Tower's of Hanoi simulation in Emacs.
execl("/usr/new/emacs", "-f","hanoi","9","-kill",0);
execl("/usr/local/emacs","-f","hanoi","9","-kill",0); // same as above
fatal("You are in a maze of twisty compiler features, all different");</pre>
<h4>正确检测整型溢出</h4>
<p>在看过编译器的这些行为后，你应该会明白——“<strong>在整型溢出之前，一定要做检查，不然，就太晚了</strong>”。</p>
<p>我们来看一段代码：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="c"> void foo(int m, int n)
{
    size_t s = m + n;
    .......
}</pre>
<p>上面这段代码有两个风险：<strong>1）有符号转无符号</strong>，<strong>2）整型溢出</strong>。这两个情况在前面的那些示例中你都应该看到了。<strong>所以，你千万不要把任何检查的代码写在 s = m + n 这条语名后面，不然就太晚了</strong>。undefined行为就会出现了——用句纯正的英文表达就是——“Dragon is here”——你什么也控制不住了。（注意：有些初学者也许会以为size_t是无符号的，而根据优先级 m 和 n 会被提升到unsigned int。其实不是这样的，m 和 n 还是signed int，m + n 的结果也是signed int，然后再把这个结果转成unsigned int 赋值给s）</p>
<p>比如，下面的代码是错的：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="c"> void foo(int m, int n)
{
    size_t s = m + n;
    if ( m&gt;0 &amp;&amp; n&gt;0 &amp;&amp; (SIZE_MAX - m &lt; n) ){
        //error handling...
    }
}</pre>
<p>上面的代码中，大家要注意 <strong>(SIZE_MAX &#8211; m &lt; n)</strong> 这个判断，为什么不用m + n &gt; SIZE_MAX呢？因为，如果 m + n 溢出后，就被截断了，所以表达式恒真，也就检测不出来了。另外，这个表达式中，m和n分别会被提升为unsigned。</p>
<p>但是上面的代码是错的，因为：</p>
<p style="padding-left: 30px;">1）检查的太晚了，if之前编译器的undefined行为就已经出来了（你不知道什么会发生）。</p>
<p style="padding-left: 30px;">2）就像前面说的一样，(SIZE_MAX &#8211; m &lt; n) 可能会被编译器优化掉。</p>
<p style="padding-left: 30px;">3）另外，SIZE_MAX是size_t的最大值，size_t在64位系统下是64位的，严谨点应该用INT_MAX或是UINT_MAX</p>
<p> 所以，正确的代码应该是下面这样：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="c"> void foo(int m, int n)
{
    size_t s = 0;
    if ( m&gt;0 &amp;&amp; n&gt;0 &amp;&amp; ( UINT_MAX - m &lt; n ) ){
        //error handling...
        return;
    }
    s = (size_t)m + (size_t)n;
}</pre>
<p>在《<a href="https://developer.apple.com/library/ios/documentation/Security/Conceptual/SecureCodingGuide/SecureCodingGuide.pdf" target="_blank" rel="noopener noreferrer">苹果安全编码规范</a>》（PDF）中，第28页的代码中：</p>
<p><img decoding="async" loading="lazy" class="aligncenter size-full wp-image-11472" src="https://coolshell.cn/wp-content/uploads/2014/04/apple_security_code.jpg" alt="" width="300" height="94" /></p>
<p>如果n和m都是signed int，那么这段代码是错的。正确的应该像上面的那个例子一样，至少要在n<em>m时要把 n 和 m 给 cast 成 size_t。因为，n</em>m可能已经溢出了，已经undefined了，undefined的代码转成size_t已经没什么意义了。（如果m和n是unsigned int，也会溢出），上面的代码仅在m和n是size_t的时候才有效。</p>
<p>不管怎么说，《<a href="https://developer.apple.com/library/ios/documentation/Security/Conceptual/SecureCodingGuide/SecureCodingGuide.pdf" target="_blank" rel="noopener noreferrer">苹果安全编码规范</a>》绝对值得你去读一读。</p>
<h5>二分取中搜索算法中的溢出</h5>
<p>我们再来看一个二分取中搜索算法（binary search），大多数人都会写成下面这个样子：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="c">int binary_search(int a[], int len, int key)
{
    int low = 0; 
    int high = len - 1; 

    while ( low&lt;=high ) {
        int mid = (low + high)/2;
        if (a[mid] == key) {
            return mid;
        }
        if (key &lt; a[mid]) {
            high = mid - 1;
        }else{
            low = mid + 1;
        }
    }
    return -1;
}</pre>
<p>上面这个代码中，你可能会有这样的想法：</p>
<p>1） 我们应该用size_t来做len, low, high, mid这些变量的类型。没错，应该是这样的。但是如果这样，你要小心第四行 int high = len -1; 如果len为0，那么就“high大发了”。</p>
<p>2） 无论你用不用size_t。我们在计算mid = (low+high)/2; 的时候，(low + high) 都可以溢出。正确的写法应该是：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="c">int mid = low + (high - low)/2;</pre>
<h5>上溢出和下溢出的检查</h5>
<p>前面的代码只判断了正数的上溢出overflow，没有判断负数的下溢出underflow。让们来看看怎么判断：</p>
<p>对于加法，还好。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="c">#include &lt;limits.h&gt;

void f(signed int si_a, signed int si_b) {
    signed int sum;
    if (((si_b &gt; 0) &amp;&amp; (si_a &gt; (INT_MAX - si_b))) ||
        ((si_b &lt; 0) &amp;&amp; (si_a &lt; (INT_MIN - si_b)))) {
        /* Handle error */
        return;
    }
    sum = si_a + si_b;
}</pre>
<p>对于乘法，就会很复杂（下面的代码太夸张了）：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="c">void func(signed int si_a, signed int si_b)
{
  signed int result;
  if (si_a &gt; 0) {  /* si_a is positive */
    if (si_b &gt; 0) {  /* si_a and si_b are positive */
      if (si_a &gt; (INT_MAX / si_b)) {
        /* Handle error */
      }
    } else { /* si_a positive, si_b nonpositive */
      if (si_b &lt; (INT_MIN / si_a)) {
        /* Handle error */
      }
    } /* si_a positive, si_b nonpositive */
  } else { /* si_a is nonpositive */
    if (si_b &gt; 0) { /* si_a is nonpositive, si_b is positive */
      if (si_a &lt; (INT_MIN / si_b)) {
        /* Handle error */
      }
    } else { /* si_a and si_b are nonpositive */
      if ( (si_a != 0) &amp;&amp; (si_b &lt; (INT_MAX / si_a))) {
        /* Handle error */
      }
    } /* End if si_a and si_b are nonpositive */
  } /* End if si_a is nonpositive */

  result = si_a * si_b;
}</pre>
<p>更多的防止在操作中整型溢出的安全代码可以参看《<a href="https://www.securecoding.cert.org/confluence/display/seccode/INT32-C.+Ensure+that+operations+on+signed+integers+do+not+result+in+overflow">INT32-C. Ensure that operations on signed integers do not result in overflow</a>》</p>
<h4>其它</h4>
<p>对于C++来说，你应该使用STL中的numeric_limits::max() 来检查溢出。</p>
<p>另外，微软的SafeInt类是一个可以帮你远理上面这些很tricky的类，下载地址：<a href="http://safeint.codeplex.com/" target="_blank" rel="noopener noreferrer">http://safeint.codeplex.com/</a></p>
<p>对于Java 来说，一种是用JDK 1.7中Math库下的safe打头的函数，如safeAdd()和safeMultiply()，另一种用更大尺寸的数据类型，最大可以到BigInteger。</p>
<p>可见，写一个安全的代码并不容易，尤其对于C/C++来说。对于黑客来说，他们只需要搜一下开源软件中代码有memcpy/strcpy之类的地方，然后看一看其周边的代码，是否可以通过用户的输入来影响，如果有的话，你就惨了。</p>
<p><strong>参考</strong>：</p>
<ul>
<li><a href="http://phrack.org/issues/60/10.html" target="_blank" rel="noopener noreferrer">Basic Integer Overflow</a></li>
</ul>
<ul>
<li><a href="https://www.owasp.org/index.php/Integer_overflow" target="_blank" rel="noopener noreferrer">OWASP：Integer overflow</a></li>
</ul>
<ul>
<li><a href="https://www.kb.cert.org/vuls/id/162289" target="_blank" rel="noopener noreferrer">C compilers may silently discard some wraparound checks</a></li>
</ul>
<ul>
<li><a href="https://developer.apple.com/library/ios/documentation/Security/Conceptual/SecureCodingGuide/SecureCodingGuide.pdf" target="_blank" rel="noopener noreferrer">Apple Secure Coding Guide</a></li>
</ul>
<ul>
<li><a href="http://en.wikipedia.org/wiki/Undefined_behavior" target="_blank" rel="noopener noreferrer">Wikipedia: Undefined Behavior</a></li>
</ul>
<ul>
<li>
<p id="title-text" class="with-breadcrumbs"><a href="https://www.securecoding.cert.org/confluence/display/seccode/INT32-C.+Ensure+that+operations+on+signed+integers+do+not+result+in+overflow">INT32-C. Ensure that operations on signed integers do not result in overflow</a></p>
</li>
</ul>
<p>最后， 不好意思，这篇文章可能罗嗦了一些，大家见谅。</p>
<p>（全文完）<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/21708.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2022/01/iStock-1175502114-150x150.png" alt="网络数字身份认证术" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21708.html" class="wp_rp_title">网络数字身份认证术</a></li><li ><a href="https://coolshell.cn/articles/20845.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/03/rust-social-wide-150x150.jpg" alt="Rust语言的编程范式" width="150" height="150" /></a><a href="https://coolshell.cn/articles/20845.html" class="wp_rp_title">Rust语言的编程范式</a></li><li ><a href="https://coolshell.cn/articles/19395.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2019/05/Authorization-360x200-1-150x150.png" alt="HTTP API 认证授权术" width="150" height="150" /></a><a href="https://coolshell.cn/articles/19395.html" class="wp_rp_title">HTTP API 认证授权术</a></li><li ><a href="https://coolshell.cn/articles/18360.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2018/05/300x262-150x150.jpg" alt="程序员练级攻略（2018)  与我的专栏" width="150" height="150" /></a><a href="https://coolshell.cn/articles/18360.html" class="wp_rp_title">程序员练级攻略（2018)  与我的专栏</a></li><li ><a href="https://coolshell.cn/articles/18094.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2017/08/enable-https-banner-150x150.png" alt="如何免费的让网站启用HTTPS" width="150" height="150" /></a><a href="https://coolshell.cn/articles/18094.html" class="wp_rp_title">如何免费的让网站启用HTTPS</a></li><li ><a href="https://coolshell.cn/articles/18024.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2017/07/api-design-300x278-2-150x150.jpg" alt="API设计原则 &#8211; Qt官网的设计实践总结" width="150" height="150" /></a><a href="https://coolshell.cn/articles/18024.html" class="wp_rp_title">API设计原则 &#8211; Qt官网的设计实践总结</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/11466.html">C语言的整型溢出问题</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/11466.html/feed</wfw:commentRss>
			<slash:comments>96</slash:comments>
		
		
			</item>
		<item>
		<title>C语言结构体里的成员数组和指针</title>
		<link>https://coolshell.cn/articles/11377.html</link>
					<comments>https://coolshell.cn/articles/11377.html#comments</comments>
		
		<dc:creator><![CDATA[陈皓]]></dc:creator>
		<pubDate>Tue, 01 Apr 2014 00:17:15 +0000</pubDate>
				<category><![CDATA[C/C++语言]]></category>
		<category><![CDATA[编程语言]]></category>
		<category><![CDATA[C++]]></category>
		<guid isPermaLink="false">http://coolshell.cn/?p=11377</guid>

					<description><![CDATA[<p>单看这文章的标题，你可能会觉得好像没什么意思。你先别下这个结论，相信这篇文章会对你理解C语言有帮助。这篇文章产生的背景是在微博上，看到@Laruence同学出了...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/11377.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/11377.html">C语言结构体里的成员数组和指针</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script>单看这文章的标题，你可能会觉得好像没什么意思。你先别下这个结论，相信这篇文章会对你理解C语言有帮助。这篇文章产生的背景是在微博上，看到<a title="Laruence" href="http://weibo.com/laruence" target="_blank">@Laruence</a>同学出了一个关于C语言的题，<a href="http://weibo.com/1170999921/ADojDbuSe" target="_blank">微博链接</a>。微博截图如下。我觉得好多人对这段代码的理解还不够深入，所以写下了这篇文章。</p>
<p style="text-align: center;"><a href="http://weibo.com/1170999921/ADojDbuSe" target="_blank"><img decoding="async" loading="lazy" class="aligncenter size-full wp-image-11378" alt="zero_array" src="https://coolshell.cn/wp-content/uploads/2014/03/zero_array.png" width="549" height="204" srcset="https://coolshell.cn/wp-content/uploads/2014/03/zero_array.png 549w, https://coolshell.cn/wp-content/uploads/2014/03/zero_array-300x111.png 300w" sizes="(max-width: 549px) 100vw, 549px" /></a></p>
<p>为了方便你把代码copy过去编译和调试，我把代码列在下面：</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">#include &lt;stdio.h&gt;
struct str{
    int len;
    char s[0];
};

struct foo {
    struct str *a;
};

int main(int argc, char** argv) {
    struct foo f={0};
    if (f.a-&gt;s) {
        printf( f.a-&gt;s);
    }
    return 0;
}
</pre>
<p>你编译一下上面的代码，在VC++和GCC下都会在14行的printf处crash掉你的程序。<a title="Laruence" href="http://weibo.com/laruence" target="_blank">@Laruence</a> 说这个是个经典的坑，我觉得这怎么会是经典的坑呢？上面这代码，你一定会问，为什么if语句判断的不是f.a？而是f.a里面的数组？写这样代码的人脑子里在想什么？还是用这样的代码来玩票？不管怎么样，看过原微博的回复，我个人觉得大家主要还是对C语言理解不深，如果这算坑的话，那么全都是坑。</p>
<p><span id="more-11377"></span></p>
<p>接下来，你调试一下，或是你把14行的printf语句改成：</p>
<p><code data-enlighter-language="c" class="EnlighterJSRAW">printf(&quot;%x\n&quot;, f.a-&gt;s);</code></p>
<p>你会看到程序不crash了。程序输出：4。 这下你知道了，访问0x4的内存地址，不crash才怪。于是，你一定会有如下的问题：</p>
<p style="padding-left: 30px;"><strong>1）为什么不是 13行if语句出错？f.a被初始化为空了嘛，用空指针访问成员变量为什么不crash？</strong></p>
<p style="padding-left: 30px;"><strong>2）为什么会访问到了0x4的地址？靠，4是怎么出来的？</strong></p>
<p style="padding-left: 30px;"><strong>3）代码中的第4行，char s[0] 是个什么东西？零长度的数组？为什么要这样玩？</strong></p>
<p>让我们从基础开始一点一点地来解释C语言中这些诡异的问题。</p>
<h4>结构体中的成员</h4>
<p>首先，我们需要知道——<strong>所谓变量，其实是内存地址的一个抽像名字罢了</strong>。在静态编译的程序中，所有的变量名都会在编译时被转成内存地址。机器是不知道我们取的名字的，只知道地址。</p>
<p>所以有了——栈内存区，堆内存区，静态内存区，常量内存区，我们代码中的所有变量都会被编译器预先放到这些内存区中。</p>
<p>有了上面这个基础，我们来看一下结构体中的成员的地址是什么？我们先简单化一下代码：</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">struct test{
    int i;
    char *p;
};</pre>
<p>上面代码中，test结构中i和p指针，在C的编译器中保存的是相对地址——也就是说，他们的地址是相对于struct test的实例的。如果我们有这样的代码：</p>
<p><code data-enlighter-language="c" class="EnlighterJSRAW">struct test t;</code></p>
<p>我们用gdb跟进去，对于实例t，我们可以看到：</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW"># t实例中的p就是一个野指针
(gdb) p t
$1 = {i = 0, c = 0 &#039;&#092;&#048;00&#039;, d = 0 &#039;&#092;&#048;00&#039;, p = 0x4003e0 &quot;1\355I\211\...&quot;}

# 输出t的地址
(gdb) p &amp;t
$2 = (struct test *) 0x7fffffffe5f0

#输出(t.i)的地址
(gdb) p &amp;(t.i)
$3 = (char **) 0x7fffffffe5f0

#输出(t.p)的地址
(gdb) p &amp;(t.p)
$4 = (char **) 0x7fffffffe5f4</pre>
<p>我们可以看到，t.i的地址和t的地址是一样的，t.p的址址相对于t的地址多了个4。说白了，<strong>t.i 其实就是(&amp;t + 0x0)</strong>, <strong>t.p 的其实就是 (&amp;t + 0x4)</strong>。0x0和0x4这个偏移地址就是成员i和p在编译时就被编译器给hard code了的地址。于是，你就知道，<strong>不管结构体的实例是什么——访问其成员其实就是加成员的偏移量</strong>。</p>
<p>下面我们来做个实验：</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">struct test{
    int i;
    short c;
    char *p;
};

int main(){
    struct test *pt=NULL;
    return 0;
}</pre>
<p>编译后，我们用gdb调试一下，当初始化pt后，我们看看如下的调试：（我们可以看到就算是pt为NULL，访问其中的成员时，其实就是在访问相对于pt的内址）</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">(gdb) p pt
$1 = (struct test *) 0x0
(gdb) p pt-&gt;i
Cannot access memory at address 0x0
(gdb) p pt-&gt;c
Cannot access memory at address 0x4
(gdb) p pt-&gt;p
Cannot access memory at address 0x8</pre>
<p>注意：上面的pt-&gt;p的偏移之所以是0x8而不是0x6，是因为内存对齐了（我在64位系统上）。关于内存对齐，可参看《<a title="深入理解C语言" href="https://coolshell.cn/articles/5761.html" target="_blank" rel="bookmark">深入理解C语言</a>》一文。</p>
<p>好了，现在你知道为什么原题中会访问到了0x4的地址了吧，因为是相对地址。</p>
<p>相对地址有很好多处，其可以玩出一些有意思的编程技巧，比如把C搞出面向对象式的感觉来，你可以参看我正好11年前的文章《<a href="http://blog.csdn.net/haoel/article/details/2864" target="_blank">用C写面向对像的程序</a>》（用指针类型强转的危险玩法——相对于C++来说，C++编译器帮你管了继承和虚函数表，语义也清楚了很多）</p>
<h4>指针和数组的差别</h4>
<p>有了上面的基础后，你把源代码中的struct str结构体中的char s[0];改成char *s;试试看，你会发现，在13行if条件的时候，程序因为Cannot access memory就直接挂掉了。为什么声明成char s[0]，程序会在14行挂掉，而声明成char *s，程序会在13行挂掉呢？<strong>那么char *s 和 char s[0]有什么差别呢</strong>？</p>
<p>在说明这个事之前，有必要看一下汇编代码，用GDB查看后发现：</p>
<ul>
<li>对于char s[0]来说，汇编代码用了lea指令，lea   0x04(%rax),   %rdx</li>
<li>对于char*s来说，汇编代码用了mov指令，mov 0x04(%rax),   %rdx</li>
</ul>
<p>lea全称load effective address，是把地址放进去，而mov则是把地址里的内容放进去。所以，就crash了。</p>
<p>从这里，我们可以看到，<strong>访问成员数组名其实得到的是数组的相对地址，而访问成员指针其实是相对地址里的内容</strong>（这和访问其它非指针或数组的变量是一样的）</p>
<p>换句话说，<strong>对于数组 char s[10]来说，数组名 s 和 &amp;s 都是一样的</strong>（不信你可以自己写个程序试试）。在我们这个例子中，也就是说，都表示了偏移后的地址。这样，如果我们访问 指针的地址（或是成员变量的地址），那么也就不会让程序挂掉了。</p>
<p>正如下面的代码，可以运行一点也不会crash掉（你汇编一下你会看到用的都是lea指令）：</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">struct test{
    int i;
    short c;
    char *p;
    char s[10];
};

int main(){
    struct test *pt=NULL;
    printf(&quot;&amp;s = %x\n&quot;, pt-&gt;s); //等价于 printf(&quot;%x\n&quot;, &amp;(pt-&gt;s) );
    printf(&quot;&amp;i = %x\n&quot;, &amp;pt-&gt;i); //因为操作符优先级，我没有写成&amp;(pt-&gt;i)
    printf(&quot;&amp;c = %x\n&quot;, &amp;pt-&gt;c);
    printf(&quot;&amp;p = %x\n&quot;, &amp;pt-&gt;p);
    return 0;
}</pre>
<p><strong>看到这里，你觉得这能算坑吗？不要出什么事都去怪语言，大家要想想是不是问题出在自己身上。</strong></p>
<h4>关于零长度的数组</h4>
<p>首先，我们要知道，<strong>0长度的数组在ISO C和C++的规格说明书中是不允许的</strong>。这也就是为什么在VC++2012下编译你会得到一个警告：“arning C4200: 使用了非标准扩展 : 结构/联合中的零大小数组”。</p>
<p>那么为什么gcc可以通过而连一个警告都没有？那是因为gcc 为了预先支持C99的这种玩法，所以，让“零长度数组”这种玩法合法了。关于GCC对于这个事的文档在这里：“<a title="Arrays of Length Zero" href="http://gcc.gnu.org/onlinedocs/gcc/Zero-Length.html" target="_blank">Arrays of Length Zero</a>”，文档中给了一个例子（我改了一下，改成可以运行的了）：</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">#include &lt;stdlib.h&gt;
#include &lt;string.h&gt;

struct line {
   int length;
   char contents[0]; // C99的玩法是：char contents[]; 没有指定数组长度
};

int main(){
    int this_length=10;
    struct line *thisline = (struct line *)
                     malloc (sizeof (struct line) + this_length);
    thisline-&gt;length = this_length;
    memset(thisline-&gt;contents, &#039;a&#039;, this_length);
    return 0;
}</pre>
<p>上面这段代码的意思是：我想分配一个不定长的数组，于是我有一个结构体，其中有两个成员，一个是length，代表数组的长度，一个是contents，代码数组的内容。后面代码里的 this_length（长度是10）代表是我想分配的数据的长度。（这看上去是不是像一个C++的类？）这种玩法英文叫：Flexible Array，中文翻译叫：柔性数组。</p>
<p>我们来用gdb看一下：</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">(gdb) p thisline
$1 = (struct line *) 0x601010

(gdb) p *thisline
$2 = {length = 10, contents = 0x601010 &quot;\n&quot;}

(gdb) p thisline-&gt;contents
$3 = 0x601014 &quot;aaaaaaaaaa&quot;</pre>
<p>我们可以看到：在输出*thisline时，我们发现其中的成员变量contents的地址居然和thisline是一样的（偏移量为0x0??!!）。但是当我们输出thisline-&gt;contents的时候，你又发现contents的地址是被offset了0x4了的，内容也变成了10个‘a’。（我觉得这是一个GDB的bug，VC++的调试器就能很好的显示）</p>
<p>我们继续，如果你sizeof(char[0])或是 sizeof(int[0]) 之类的零长度数组，你会发现sizeof返回了0，这就是说，零长度的数组是存在于结构体内的，但是不占结构体的size。你可以简单的理解为一个没有内容的占位标识，直到我们给结构体分配了内存，这个占位标识才变成了一个有长度的数组。</p>
<p>看到这里，你会说，为什么要这样搞啊，把contents声明成一个指针，然后为它再分配一下内存不行么？就像下面一样。</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW" data-enlighter-highlight="3,9">struct line {
   int length;
   char *contents;
};

int main(){
    int this_length=10;
    struct line *thisline = (struct line *)malloc (sizeof (struct line));
    thisline-&gt;contents = (char*) malloc( sizeof(char) * this_length );
    thisline-&gt;length = this_length;
    memset(thisline-&gt;contents, &#039;a&#039;, this_length);
    return 0;
}</pre>
<p>这不一样清楚吗？而且也没什么怪异难懂的东西。是的，这也是普遍的编程方式，代码是很清晰，也让人很容易理解。即然这样，那为什么要搞一个零长度的数组？有毛意义？！</p>
<p>这个事情出来的原因是——<strong>我们想给一个结构体内的数据分配一个连续的内存！</strong>这样做的意义有两个好处：</p>
<p style="padding-left: 30px;"><strong>第一个意义是，方便内存释放</strong>。如果我们的代码是在一个给别人用的函数中，你在里面做了二次内存分配，并把整个结构体返回给用户。用户调用free可以释放结构体，但是用户并不知道这个结构体内的成员也需要free，所以你不能指望用户来发现这个事。所以，如果我们把结构体的内存以及其成员要的内存一次性分配好了，并返回给用户一个结构体指针，用户做一次free就可以把所有的内存也给释放掉。（读到这里，你一定会觉得C++的封闭中的析构函数会让这事容易和干净很多）</p>
<p style="padding-left: 30px;"><strong>第二个原因是，这样有利于访问速度</strong>。连续的内存有益于提高访问速度，也有益于减少内存碎片。（其实，我个人觉得也没多高了，反正你跑不了要用做偏移量的加法来寻址）</p>
<p>我们来看看是怎么个连续的，用gdb的x命令来查看：(我们知道，用struct line {}中的那个char contents[]不占用结构体的内存，所以，struct line就只有一个int成员，4个字节，而我们还要为contents[]分配10个字节长度，所以，一共是14个字节)</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">(gdb) x /14b thisline
0x601010:       10      0       0       0       97      97      97      97
0x601018:       97      97      97      97      97      97</pre>
<p>从上面的内存布局我们可以看到，前4个字节是 int length，后10个字节就是char contents[]。</p>
<p>如果用指针的话，会变成这个样子：</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">(gdb) x /16b thisline
0x601010:       1       0       0       0       0       0       0       0
0x601018:       32      16      96      0       0       0       0       0
(gdb) x /10b this-&gt;contents
0x601020:       97      97      97      97      97      97      97      97
0x601028:       97      97</pre>
<p>上面一共输出了四行内存，其中，</p>
<ul>
<li>第一行前四个字节是 int length，第一行的后四个字节是对齐。</li>
<li>第二行是char* contents，64位系统指针8个长度，他的值是0x20 0x10 0x60 也就是0x601020。</li>
<li>第三行和第四行是char* contents指向的内容。</li>
</ul>
<p>从这里，我们看到，<strong>其中的差别——数组的原地就是内容，而指针的那里保存的是内容的地址</strong>。</p>
<h4>后记</h4>
<p>好了，我的文章到这里就结束了。但是，请允许我再唠叨两句。</p>
<p style="padding-left: 30px;"><strong>1）看过这篇文章，你觉得C复杂吗？我觉得并不简单。某些地方的复杂程度不亚于C++。</strong></p>
<p style="padding-left: 30px;"><strong>2）那些学不好C++的人一定是连C都学不好的人。连C都没学好，你们根本没有资格鄙视C++。</strong></p>
<p style="padding-left: 30px;"><strong>3）当你们在说有坑的时候，你得问一下自己，是真有坑还是自己的学习能力上出了问题。</strong></p>
<p>如果你觉得你的C语言还不错，欢迎你看看《<a title="C语言的谜题" href="https://coolshell.cn/articles/945.html" target="_blank">C语言的谜题</a>》还有《<a title="谁说C语言很简单？" href="https://coolshell.cn/articles/873.html" target="_blank">谁说C语言很简单？</a>》还有《<a href="https://coolshell.cn/articles/830.html" target="_blank">语言的歧义</a>》以及《<a title="深入理解C语言" href="https://coolshell.cn/articles/5761.html" target="_blank" rel="bookmark">深入理解C语言</a>》一文。</p>
<p>（全文完）<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="http://coolshell.cn/articles/11235.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/28.jpg" alt="一个浮点数跨平台产生的问题" width="150" height="150" /></a><a href="http://coolshell.cn/articles/11235.html" class="wp_rp_title">一个浮点数跨平台产生的问题</a></li><li ><a href="http://coolshell.cn/articles/873.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/24.jpg" alt="谁说C语言很简单？" width="150" height="150" /></a><a href="http://coolshell.cn/articles/873.html" class="wp_rp_title">谁说C语言很简单？</a></li><li ><a href="http://coolshell.cn/articles/945.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/14.jpg" alt="C语言的谜题" width="150" height="150" /></a><a href="http://coolshell.cn/articles/945.html" class="wp_rp_title">C语言的谜题</a></li><li ><a href="http://coolshell.cn/articles/830.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/18.jpg" alt="语言的歧义" width="150" height="150" /></a><a href="http://coolshell.cn/articles/830.html" class="wp_rp_title">语言的歧义</a></li><li ><a href="http://coolshell.cn/articles/5761.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/28.jpg" alt="深入理解C语言" width="150" height="150" /></a><a href="http://coolshell.cn/articles/5761.html" class="wp_rp_title">深入理解C语言</a></li><li ><a href="http://coolshell.cn/articles/9859.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2013/06/Alan-Cox-150x150.jpg" alt="Alan Cox：单向链表中prev指针的妙用" width="150" height="150" /></a><a href="http://coolshell.cn/articles/9859.html" class="wp_rp_title">Alan Cox：单向链表中prev指针的妙用</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/11377.html">C语言结构体里的成员数组和指针</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/11377.html/feed</wfw:commentRss>
			<slash:comments>195</slash:comments>
		
		
			</item>
		<item>
		<title>一个浮点数跨平台产生的问题</title>
		<link>https://coolshell.cn/articles/11235.html</link>
					<comments>https://coolshell.cn/articles/11235.html#comments</comments>
		
		<dc:creator><![CDATA[tanglei.name]]></dc:creator>
		<pubDate>Sat, 15 Mar 2014 12:44:24 +0000</pubDate>
				<category><![CDATA[.NET编程]]></category>
		<category><![CDATA[C/C++语言]]></category>
		<category><![CDATA[.NET]]></category>
		<category><![CDATA[C++]]></category>
		<category><![CDATA[float]]></category>
		<category><![CDATA[FPU]]></category>
		<category><![CDATA[SSE]]></category>
		<guid isPermaLink="false">http://coolshell.cn/?p=11235</guid>

					<description><![CDATA[<p>感谢网友唐磊（微博@唐磊_name）投稿，本文原文在唐磊的博客上（原文地址），原文分析还不够好，而且可能对人有误导，所以，我对原文做了很多修改，并加了Linux...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/11235.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/11235.html">一个浮点数跨平台产生的问题</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><strong>感谢网友<a href="http://www.tanglei.name/" target="_blank">唐磊</a>（微博@<a title="唐磊_name" href="http://weibo.com/tangleithu?from=feed&amp;loc=nickname">唐磊_name</a>）投稿，本文原文在唐磊的博客上（<a href="http://www.tanglei.name/a-bug-relate-with-float-point-between-x86-and-x64-in-csharp/">原文地址</a>），原文分析还不够好，而且可能对人有误导，所以，我对原文做了很多修改，并加了Linux下的内容。浮点数是一个很复杂的事情，希望这篇文章有助于大家了解浮点数与其相关的C/C++的编译选项。</strong>（注：我没有Windows 32位以及C#的环境，所以，对于Windows 32位的程序和C#的程序没有验证过）</p>
<p>背景就简单点儿说，最近一个项目C#编写，涉及浮点运算，来龙去脉省去，直接看如下代码。</p>
<pre data-enlighter-language="csharp" class="EnlighterJSRAW">float p3x = 80838.0f;
float p2y = -2499.0f;
double v321 = p3x * p2y;
Console.WriteLine(v321);</pre>
<p>很简单吧，马上笔算下结果为-202014162，没问题，难道C#没有产生这样的结果？不可能吧，开启Visual Studio，copy代码试试，果然结果是-202014162。就这样完了么？显然没有！你把编译时的选项从AnyCPU改成x64试试~(服务器环境正是64位滴哦！！)结果居然边成了-202014160，对没错，就是-202014160。有点不相信，再跑两遍，仍然是-202014160。呃，想通了，因为浮点运算的误差，-202014160这个结果是合理的。</p>
<p>为什么合理呢？很正常，因为上面的p3x和p2y是两个float类型，虽然v321是double，但也是两个float类型计算完后再转成double的，<strong>float的精度本来也只有7位，所以，对于这个上亿的数，自然没有办法保证精度</strong>。</p>
<p><strong>但是为什么修改CPU的type会有不同的效果？</strong>嗯，我们再试试C/C++。</p>
<p><span id="more-11235"></span></p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">#include
using namespace std;

int main()
{
    float p3x = 80838.0f;
    float p2y = -2499.0f;
    double v321 = p3x * p2y;
    std::cout.precision(15);
    std::cout &lt;&lt; v321 &lt;&lt; std::endl;

    return 0;
}
</pre>
<p>上面这段C++代码在不同的平台下的结果如下：</p>
<ul>
<li>Windows 32/64位下：-202014160</li>
<li>Linux 64位下（CentOS 6 gcc 4.4.7）-202014160，</li>
<li>Linux 32位下（Ubuntu 12.04+ gcc 4.6.3）是：-202014162</li>
</ul>
<p><strong>合理的结果应该是-202014160，正确的运算结果是-202014162</strong>，合理性是浮点精度不够造成的（文后解释了合理性）。若是用两个double相乘可得正确且合理的运算结果（注：把上面C++的程序中的p3x和p2y的类型声明成double，就能得到正确的结果，因为double是双精度的，float是单精度，所以double有足够的位数存放更多的数位）。<strong>但是我们有点不明白，为什么Linux 32位下，居然能算出“正确”的数，而不是“合理”的数</strong>。</p>
<p>与C++一样，C#在32位和64位（DEBUG下，这个后面会说）下没有得到一致的结果，那我们来看一下C++/C#的汇编代码（使用gdb的disassemble /m main 命令，另外下面只显示 float * float 然后转成double的那一行代码的汇编）</p>
<p><strong>Linux平台下用G++编译</strong></p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">//C++ 32位系统下 Ubuntu 12.04
8	    double v321 = p3x * p2y;
   0x0804860f &lt;+27&gt;:	flds   0x18(%esp)
   0x08048613 &lt;+31&gt;:	fmuls  0x1c(%esp)
   0x08048617 &lt;+35&gt;:	fstpl  0x10(%esp)

.......</pre>
<pre data-enlighter-language="c" class="EnlighterJSRAW">//C++ 64位系统下 CentOS 6
9           double v321 = p3x * p2y;
   0x000000000040083c &lt;+24&gt;:    movss  -0x20(%rbp),%xmm0
   0x0000000000400841 &lt;+29&gt;:    mulss  -0x1c(%rbp),%xmm0
   0x0000000000400846 &lt;+34&gt;:    unpcklps %xmm0,%xmm0
   0x0000000000400849 &lt;+37&gt;:    cvtps2pd %xmm0,%xmm0
   0x000000000040084c &lt;+40&gt;:    movsd  %xmm0,-0x18(%rbp)</pre>
<p><strong>Windows平台下用Visual Studio编译</strong></p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">//C# AnyCPU编译，Windows VS2012
double v321 = p3x * p2y;
00000049  fld         dword ptr [ebp-40h]
0000004c  fmul        dword ptr [ebp-44h]
0000004f  fstp        qword ptr [ebp-4Ch]</pre>
<pre data-enlighter-language="c" class="EnlighterJSRAW">//C# X64位编译 Windows7 VS2012
double v321 = p3x * p2y;&lt;/pre&gt;
009B43B8 movss xmm0,dword ptr [p3x]
009B43BD mulss xmm0,dword ptr [p2y]
009B43C2 cvtss2sd xmm0,xmm0
009B43C6 movsd mmword ptr [v321],xmm0</pre>
<p>从上面的汇编代码可以看出，无论是Linux和Windows，C++或C# 32位和64对浮点数的汇编指令并不一样。 32位生成代码用的指令是fld/fmul/fstp等，而64位下的使用了movss/mulss/movsd/的指令。看下来，似乎这个事情和平台有关系。</p>
<p>我们继续调查，我们发现，其中fld/fmul/fstp等指令是由<strong>FPU</strong>(float point unit)浮点运算处理器做的，准确的说，是FPU x87指令，FPU在进行浮点运算时，用了<strong>80位</strong>的寄存器做相关浮点运算，然后再根据是float/double截取成32位或64位，FPU默认上会尽量减少由于需要四舍五入带来的精度问题。可参看浮点运算标准<a href="http://en.wikipedia.org/wiki/IEEE_floating_point" target="_blank">IEEE-754</a> 推荐标准实现者提供浮点可扩展精度格式(<a href="http://en.wikipedia.org/wiki/Extended_precision" target="_blank">Extended precision</a>)，Intel x86处理器有FPU(float point unit)浮点运算处理器支持这种扩展。</p>
<p>非FPU的情况是用了SSE中128位寄存器(float实际只用了其中的32位，计算时也是以32位计算的)，这就是导致上述问题产生的最终原因。详细分析见文末说明。</p>
<p>知道了这一点，我们可以man g++ 看一下文档，我们可以找到一个编译选项叫：<strong>-mfpmath，在32位下，这个编译选项的默认值是：387，也就是x87 FPU指令，在64位下，这个编译选项的值是sse，也就是使用SSE的指令</strong>。所以，就这篇文章中的这个例子而言，如果你在64bits下加上如 -mfpmath=387，你会得到“正确的”结果，而不是“合理的”结果。</p>
<p>而在VS2012中C++，<a href="http://msdn.microsoft.com/zh-cn/library/vstudio/e7s85ffb(v=vs.110).aspx" target="_blank">编译选项可以设置(代码生成中)</a>可选，/fp:[precise | fast | strict]，本例中Release 32位下用precise 或者 strict将得到合理的结果(-202014160)，fast将产生正确的结果(-202014162), fast debug/release下结果也不一样哦(release下才优化了)。64系统下各个结果可以大家自己去测试下(Debug/Release)，分别看看VS编译后产生的中间代码长什么样。（陈皓注：我的VS2012在debug编译下，无论你怎么设置/fp的参数值，汇编都是一样的，使用SSE指令，而Release就不一样了，但是我的release下看代码的汇编非常怪异和源代码对上号，多年不用Windows开发了，对VS的使用仅停留在VC6++/VC2005上）</p>
<p>所以，我们在从x87 FPU指令向SSE指令做代码移植的时候，我们可能会遇到向这样的浮点数的精度问题，这个精度问题会多次科学计算中会更糟糕。<strong>这个问题并不简单的只是在32位和64位中的系统出算，这个问题主要还是看语言编译器的实现</strong>。在更为高级的语言中，如：C99或Fortran 2003中，引入了“long double”来做可扩展双精度（Extension Double），这样就可以消除更多的精度问题。</p>
<p>下面我们把程序改成long double，（注：其中的类型变成long double）</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">#include
using namespace std;

int main()
{
    long double p3x = 80838.0;
    long double p2y = -2499.0;
    long double v321 = p3x * p2y;
    std::cout.precision(15);
    std::cout &lt;&lt; v321 &lt;&lt; std::endl;

    return 0;
}</pre>
<p>用gdb的disassemble /m main你会看到其中的运算的汇编如下（使用了fmlp指令）：</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">//linux 32位系统
8	    long double v321 = p3x * p2y;
   0x08048633 &lt;+63&gt;:	fldt   0x10(%esp)
   0x08048637 &lt;+67&gt;:	fldt   0x20(%esp)
   0x0804863b &lt;+71&gt;:	fmulp  %st,%st(1)
   0x0804863d &lt;+73&gt;:	fstpt  0x30(%esp)
</pre>
<pre data-enlighter-language="c" class="EnlighterJSRAW">//linux 64位系统
8           long double v321 = p3x * p2y;
   0x0000000000400818 &lt;+52&gt;:    fldt   -0x30(%rbp)
   0x000000000040081b &lt;+55&gt;:    fldt   -0x20(%rbp)
   0x000000000040081e &lt;+58&gt;:    fmulp  %st,%st(1)
   0x0000000000400820 &lt;+60&gt;:    fstpt  -0x10(%rbp)
</pre>
<p><span style="line-height: 1.5em;">我们可以看到，32位系统和64位系统使用了同样的汇编指令（当然，我没有那么多物理机，我只是在VMWare Play的虚拟机上测试的，所以上面的示例并不一定适用于所有的地方，另外，C/C++语言和编译器和平台有非常大的关系） ，原因自然是我们用到了long double这个扩展双精度的数据类型。（注：如果你用double或float，在Linux上，32位用x87 FPU 指令编译，而64位用SSE指令编译）</span></p>
<p>好了，我们再回到C#上来，<span style="line-height: 1.5em;">C#的浮点是支持该标准的，其中</span><a style="line-height: 1.5em;" href="http://msdn.microsoft.com/en-us/library/aa691146(v=vs.71).aspx">其官方文档</a><span style="line-height: 1.5em;">也提到了浮点运算可能会产生比返回类型更高精度的值（正如上面的返回值精度就超过了float的精度），并说明如果硬件支持可扩展浮点精度的话，那么</span><strong style="line-height: 1.5em;">所有的</strong><span style="line-height: 1.5em;">浮点运算都将用此精度进行以提高效率，举个例子x*y/z, x*y的值可能都在double的能力范围之外了，但真实情况可能除以z后又能把结果拉回到double范围内，这样的话，用了FPU的结果就会得到一个准确的double值，而非FPU的就是无穷大之类的了。</span></p>
<p><span style="line-height: 1.5em;">所以，对于</span>C#来说，你显然无法找到一个像C/C++一样的利用编译器选项的来解决这个问题的“解决方案”（其实，用编译器参数是一个伪解决方案）<span style="line-height: 1.5em;">。</span></p>
<p><span style="line-height: 1.5em;"><strong>而且，要解决这个问题也不是要修改编译器选项，因为这个问题明显不是FPU或是SSE的问题，FPU是个过时的技术，SSE才是合理的技术，所以，<span style="color: #cc0000;">如果你不想你的浮点数在计算上有什么问题，而且你需要精度准确，正确的解决方案不是搞编译参数，而是——你一定要使用精度更高字节数更多的数据类型，比如：double 或是long double</span>。</strong></span></p>
<p>另外，大家在写代码的时候得保证实际运行环境/测试环境/开发环境的<strong>一致性(包括OS架构啊、编译选项等)</strong>啊（<strong>尤其是C/C++ 而且，编译器上的参数可能会有很多坑，而且有些坑可能会掩盖你程序中的问题</strong>），不然莫名其妙的问题会产生（本文就是开发环境与运行环境不一致导致的问题，纠结了好久才发现是这个原因）；遇到涉及浮点运算的时候别忘了有可能是这个原因产生的；<strong>float/double混用的情况得特别注意</strong>。</p>
<p><strong>Reference：</strong></p>
<p>[1] <a href="http://msdn.microsoft.com/en-us/library/aa691146(v=vs.71).aspx">C# Language Specification Floating point types</a><br />
[2] <a href="http://stackoverflow.com/questions/6683059/are-floating-point-numbers-consistent-in-c-can-they-be">Are floating-point numbers consistent in C#? Can they be? </a><br />
[3] <a href="http://www.plantation-productions.com/Webster/www.artofasm.com/Linux/HTML/RealArithmetica2.html">The FPU Instruction Set</a></p>
<h4><strong>附录</strong></h4>
<h5><strong>80838.0f * -2499.0f = -202014160.0浮点运算过程的说明</strong></h5>
<p>32位浮点数在计算机中的表示方式为：1位符号位(s)-8位指数位(E)-23位有效数字(M)。<br />
32位Float = (-1)^s * (1+m) * 2^(e-127), 其中e是实际转换成1.xxxxx*2^e的指数,m是前面的xxxxx(节约1位)</p>
<p>80838.0f = 1 0011 1011 1100 0110.0= 1.00111011110001100*2^16<br />
有效位M = 0011 1011 1100 0110 0000 000<br />
指数位E = 16 + 127 = 143 =  10001111<br />
内部表示 80838.0 =  0 [1000 1111] [0011 1011 1100 0110 0000 000]<br />
= 0100 0111 1001 1101 1110 0011 0000 0000<br />
= 47 9d e3 00 //实际调试时看到的内存值 可能是00 e3 9d 47是因为调试环境用了小端表示法法：低位字节排内存低地址端，高位排内存高地址</p>
<p>-2499.0 = -100111000011.0 = -1.001110000110 * 2^11<br />
有效位M = 0011 1000 0110 0000 0000 000<br />
指数位E = 11+127=138= 10001010<br />
符号位s = 1<br />
内部表示-2499.0 = 1 [10001010] [0011 1000 0110 0000 0000 000]<br />
=1100 0101 0001 1100 0011 0000 0000 0000<br />
=c5 1c 30 00</p>
<p>80838.0 * -2499.0 = ?</p>
<p>首先是指数 e = 11+16 = 27<br />
指数位E = e + 127 = 154 = 10011010<br />
有效位相乘结果为 1.1000 0001 0100 1111 1011 1010 01 //可以自己动手实际算下<br />
实际中只能有23位，后面的被截断即1000 0001 0100 1111 1011 101<span style="text-decoration: line-through;">0 01 </span><br />
相乘结果内部表示=1[10011010][1000 0001 0100 1111 1011 101]<br />
= 1100 1101 0100 0000 1010 0111 1101 1101<br />
= cd 40 a7 dd</p>
<p>结果 =  -1.1000 0001 0100 1111 1011 101 *2^27<br />
=  -11000 0001 0100 1111 1011 1010000<br />
=  -202014160<br />
再转成double后还是-202014160.</p>
<p>如果是FPU的话，上面的有效位结果不会被截断，即<br />
FPU结果 = -1.1000 0001 0100 1111 1011 101<strong>001</strong> *2^27<br />
= -11000 0001 0100 1111 1011 101<strong>001</strong>0<br />
= -202014162</p>
<p>全文完，若本文有纰漏之处欢迎指正。<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/3008.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/10.jpg" alt="Windows编程革命简史" width="150" height="150" /></a><a href="https://coolshell.cn/articles/3008.html" class="wp_rp_title">Windows编程革命简史</a></li><li ><a href="https://coolshell.cn/articles/2672.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/0.jpg" alt=".NET代码转换器" width="150" height="150" /></a><a href="https://coolshell.cn/articles/2672.html" class="wp_rp_title">.NET代码转换器</a></li><li ><a href="https://coolshell.cn/articles/20845.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/03/rust-social-wide-150x150.jpg" alt="Rust语言的编程范式" width="150" height="150" /></a><a href="https://coolshell.cn/articles/20845.html" class="wp_rp_title">Rust语言的编程范式</a></li><li ><a href="https://coolshell.cn/articles/18360.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2018/05/300x262-150x150.jpg" alt="程序员练级攻略（2018)  与我的专栏" width="150" height="150" /></a><a href="https://coolshell.cn/articles/18360.html" class="wp_rp_title">程序员练级攻略（2018)  与我的专栏</a></li><li ><a href="https://coolshell.cn/articles/18024.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2017/07/api-design-300x278-2-150x150.jpg" alt="API设计原则 &#8211; Qt官网的设计实践总结" width="150" height="150" /></a><a href="https://coolshell.cn/articles/18024.html" class="wp_rp_title">API设计原则 &#8211; Qt官网的设计实践总结</a></li><li ><a href="https://coolshell.cn/articles/12052.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/29.jpg" alt="Leetcode 编程训练" width="150" height="150" /></a><a href="https://coolshell.cn/articles/12052.html" class="wp_rp_title">Leetcode 编程训练</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/11235.html">一个浮点数跨平台产生的问题</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/11235.html/feed</wfw:commentRss>
			<slash:comments>25</slash:comments>
		
		
			</item>
		<item>
		<title>由苹果的低级Bug想到的</title>
		<link>https://coolshell.cn/articles/11112.html</link>
					<comments>https://coolshell.cn/articles/11112.html#comments</comments>
		
		<dc:creator><![CDATA[陈皓]]></dc:creator>
		<pubDate>Mon, 24 Feb 2014 00:12:11 +0000</pubDate>
				<category><![CDATA[C/C++语言]]></category>
		<category><![CDATA[业界新闻]]></category>
		<category><![CDATA[流程方法]]></category>
		<category><![CDATA[Apple]]></category>
		<category><![CDATA[bug]]></category>
		<category><![CDATA[C++]]></category>
		<category><![CDATA[goto]]></category>
		<guid isPermaLink="false">http://coolshell.cn/?p=11112</guid>

					<description><![CDATA[<p>2014年2月22日，在这个“这么二”的日子里，苹果公司推送了 iOS 7.0.6（版本号11B651）修复了 SSL 连接验证的一个 bug。官方网页在这里：...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/11112.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/11112.html">由苹果的低级Bug想到的</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><img decoding="async" loading="lazy" class="alignright size-full wp-image-11123" src="https://coolshell.cn/wp-content/uploads/2014/02/apple_goto_fail.png" alt="" width="260" height="158" /> 2014年2月22日，在这个“这么二”的日子里，苹果公司推送了 iOS 7.0.6（版本号11B651）修复了 SSL 连接验证的一个 bug。官方网页在这里：<a href="http://support.apple.com/kb/HT6147" target="_blank">http://support.apple.com/kb/HT6147</a>，网页中如下描述：</p>
<blockquote><p><strong>Impact</strong>: An attacker with a privileged network position may capture or modify data in sessions protected by SSL/TLS</p>
<p><strong>Description</strong>: Secure Transport failed to validate the authenticity of the connection. This issue was addressed by restoring missing validation steps.</p></blockquote>
<p>也就是说，这个bug会引起中间人攻击，bug的描述中说，这个问题是因为miss了对连接认证的合法性检查的步骤。</p>
<p>这里多说一句，<strong>一旦网上发生任何的和SSL/TL相关的bug或安全问题，不管是做为用户，还是做为程序员的你，你一定要高度重视起来</strong>。因为这个网络通信的加密协议被广泛的应用在很多很多最最需要安全的地方，如果SSL/TLS有问题的话，意味着这个世界的计算机安全体系的崩溃。</p>
<h4>Bug的代码原因</h4>
<p>Adam Langley的《<a href="https://www.imperialviolet.org/2014/02/22/applebug.html">Apple&#8217;s SSL/TLS bug</a> 》的博文暴出了这个bug的细节。（在苹果的开源网站上，通过查看苹果的和SSL/TLS有关的代码变更，我们可以在文件<a href="http://opensource.apple.com/source/Security/Security-55471/libsecurity_ssl/lib/sslKeyExchange.c" target="_blank">sslKeyExchange.c</a>中找到下面的代码）</p>
<p><span id="more-11112"></span></p>
<pre data-enlighter-language="c" class="EnlighterJSRAW" data-enlighter-highlight="12">static OSStatus
SSLVerifySignedServerKeyExchange(SSLContext *ctx, bool isRsa, SSLBuffer signedParams,
                                 uint8_t *signature, UInt16 signatureLen)
{
	OSStatus        err;
	...

	if ((err = SSLHashSHA1.update(&amp;hashCtx, &amp;serverRandom)) != 0)
		goto fail;
	if ((err = SSLHashSHA1.update(&amp;hashCtx, &amp;signedParams)) != 0)
		goto fail;
		goto fail;
	if ((err = SSLHashSHA1.final(&amp;hashCtx, &amp;hashOut)) != 0)
		goto fail;
	err = sslRawVerify(ctx,
                       ctx-&gt;peerPubKey,
                       dataToSign,				/* plaintext */
                       dataToSignLen,			/* plaintext length */
                       signature,
                       signatureLen);
	if(err) {
		sslErrorLog(&quot;SSLDecodeSignedServerKeyExchange: sslRawVerify &quot;
                    &quot;returned %d\n&quot;, (int)err);
		goto fail;
	}

fail:
    SSLFreeBuffer(&amp;signedHashes);
    SSLFreeBuffer(&amp;hashCtx);
    return err;
}</pre>
<p>注意，我高亮的地方，也就是那里有两个goto fail; 因为if语句没有加大括号，所以，只有第一个goto是属于if的，而第二个goto则是永远都会被执行到的（注：这里不是Python是C语言，缩进不代表这个语句属于同一个语句块）。也就是说，就算是前面的if检查都失败了（err  == 0），也会goto fail。我们可以看到fail标签中释放完内存后就会return err;</p>
<p>你想一下，<strong>这段程序在SSLHashSHA1.update()  返回成功，也就是返回0 的时候会发生什么样的事？是的，真正干活的 sslRawVerify()被bypass了。而且这个函数SSLVerifySignedServerKeyExchange() 还返回了0，也就是成功了！</strong>尼玛！你可能想到酷壳网上之前《<a title="一个空格引发的惨剧" href="https://coolshell.cn/articles/4875.html" target="_blank">一个空格引发的惨剧</a>》的文章。都是低级bug。</p>
<p>这个低级bug在这个周末在网上被炒翻了天，你可以<strong><a href="https://twitter.com/search?q=%23gotofail" target="_blank">上Twiter上看看#gotofail的标签的盛况</a></strong>。<strong>Goto Fail必然会成为历史上的一个经典事件</strong>。</p>
<p>如果你喜欢XKCD，你一定会想到这个漫画：</p>
<p><img decoding="async" loading="lazy" class="aligncenter" src="https://sslimgs.xkcd.com/comics/goto.png" alt="" width="740" height="201" /></p>
<p><span style="line-height: 1.5em;"><strong>注意</strong>：这个bug不会影响TLS 1.2版本，因为1.2版本不会用这个函数，走的是另一套机制。但是别忘了client端是可以选择版本的。</span></p>
<p>如果你想测试一下你的浏览器是否会有问题，<strong>你可以上一下当天就上线的<a href="https://gotofail.com/" target="_blank"> https://gotofail.com</a> 网站</strong></p>
<h4>一些思考</h4>
<p>下面是我对这个问题的一些思考。</p>
<h5>0）关于编译报警</h5>
<p>有人在说苹果的这个代码中的goto语句会产生死代码——dead code，也就是永远都不会执行到的代码，C/C++的编程器是会报警的。但，实际上，dead code在默认上的不会报警的。即使你加上-Wall，GCC 4.8.2 或 Clang 3.3 都不会报警，包括Visual Studio 2012在默认的报警级别也不会（默认是/W3级，需要上升到/W4级以上，但是升级到/W4上，你的工程可能会有N多的Warning，你不一定能看得过来）。gcc和Clang有一个参数叫：-Wunreachable-code，是可以对这种情况报警的，但即没有被包括在-Wall里。原因是，这个参数有很多的问题，因为编译器的优化代码的行为，这个参数并不能对每种情况都准确地报告。另请注意，GCC的新版本中剔除了这个参数。当然，其它一些静态的代码检查工具也可以检查这个低级的问题。</p>
<p>另外，是不是用IDE的代码自动化格式工具也可以帮上一点忙呢？至少可以把那个缩进变成让人一看就觉得有问题。</p>
<h5>1）关于Code Merge 和 Code Review</h5>
<p>你可以通过这里的代码比较看到这个bug的diff，也可以到<a href="https://gist.github.com/alexyakoubian/9151610/revisions" target="_blank">这里看看</a>（631行）。</p>
<blockquote style="font-size: 11px;"><p>diff -urN &lt;(curl -s http://opensource.apple.com/source/Security/Security-55179.13/libsecurity_ssl/lib/sslKeyExchange.c\?txt) \ &lt;(curl -s http://opensource.apple.com/source/Security/Security-55471/libsecurity_ssl/lib/sslKeyExchange.c\?txt) \</p></blockquote>
<p>通过code diff你可以看到，<strong>苹果公司是在重构代码——为很多函数去掉了ctx的参数</strong>。</p>
<p>所以，我们可以猜测，两个goto fail语句，可能是因为对code在不同branch上做merge发生的。版本工具merge代码的时候，经常性的会出现这样的问题。如果代码的diff很多，这个问题会很容易就没有注意到。就算有code review，这个有问题的代码也很难被找出来的。<strong>如果你来review下面的diff，你会注意到这个错误吗？</strong></p>
<p><img decoding="async" loading="lazy" class="aligncenter" src="https://coolshell.cn/wp-content/uploads/2014/02/gotofail.jpg" alt="" width="560" height="275" /></p>
<p>也就是说，在重构分支上的代码是对的，但是在分支merge的时候，被merge工具搞乱了。所以说，<strong>我们在做code merge的时候，一定要小心小心再小心，不能完全相信merge工具</strong>。</p>
<h5>2）关于测试</h5>
<p>很明显，这个bug很难被code review发现。对于重构代码和代码merge里众多的diff，是很难被review的。</p>
<p>当然，“事后诸葛亮”的人们总是很容易地说这个问题可以被测试发现，但是实际情况是这样的吗？</p>
<p>这个问题也很难被功能测试发现，因为这个函数在是在网络握手里很深的地方，功能 测试不一定能覆盖得那么深，你要写这样的case，必需对TLS的协议栈非常熟悉，熟悉到对他所有的参数都很熟悉，并能写出针对每一个参数以及这些参数的组合做一堆test case，这个事情也是一件很复杂的事。要写出所有的case本身就是一件很难很难的事情。关于这个叫SSLVerifySignedServerKeyExchange()函数的细节，你可以看看相关的<a href="https://tools.ietf.org/html/rfc5246#section-7.4.3">ServerKeyExchange</a> RFC文档。</p>
<p>如果只看这个问题的话，你会说对这个函数做的 Unit Test 可以发现这个问题，是的。但是，别忘了SSL/TLS这么多年了，这些基础函数都应该是很稳定的了， 在事前，我们可能不会想到要去为这些稳定了多少年的函数写几个Unit Test。</p>
<p><strong>只要有足够多的时间，我们是可以对所有的功能点，所有的函数都做UT，也可以去追求做代码覆盖和分支覆盖一样。但有一点我们却永远无法做到，那就是——穷举所有的负面案例</strong>。所以，对于测试来说，我们不能走极端，需要更聪明的测试。就像我在《<a title="我们需要专职的QA吗？" href="https://coolshell.cn/articles/6994.html" target="_blank">我们需要专职的QA</a>》文章里的说过的——<strong>测试比coding难度大多了，测试这个工作只有高级的开发人员才做得好。我从来不相信不写代码的人能做好测试。</strong></p>
<p>这里，<strong>我并不是说通过测试来发现这个问题的可能性不大，我想说的是，测试很重要，单测更重要。但是，我们无法面面俱到</strong>。在我们没有关注到的地方，总会发生愚蠢的错误。</p>
<p>P.S.，在各大网站对这个事的讨论中，我们可以看到OS X下的curl命令居然可以接受一个没有验证过的IP地址的https的请求，虽然现在还没有人知道这事的原因，但是，这可能是没有在测试中查到的一个原因。</p>
<h5>3）关于编码风格</h5>
<p><span style="line-height: 1.5em;">对于程序员来说，在C语言中，省掉语句大括号是一件非常不明智 的事情。如我们强制使用语句块括号，那么，这两个goto fail都会在一个if的语句块里，而且也容易维护并且易读。（另外，通过这个bug，我们可以感受到，像Python那样，用缩进来表示语句块，的确是挺好的一件事）</span></p>
<p>也有人说，如果你硬要用只有单条语句，且不用语句块括号，那么，这就是一条语句，应该放在同一行上。如下所示：</p>
<p><code data-enlighter-language="c" class="EnlighterJSRAW">if  (check_something)   do_something(); </code></p>
<p>但是这样一来，你在单步调试代码的时候，就有点不爽了，当你step over的时候，你完全不知道if的条件是真还是假。所以，还是分多行，加上大括号会好一些。</p>
<p>相似的问题，我很十多年前也犯过，而且那次我出的问题也比较大，导致了用户的数据出错。那次就是维护别人的代码，别人的代码就是没有if的语句块括号，就像苹果的代码那样。<span style="line-height: 1.5em;">我想在return z之前调用一个函数，结果就杯具了：</span></p>
<pre data-enlighter-language="c" class="EnlighterJSRAW" data-enlighter-highlight="6">if ( ...... )
    return x;
if ( ...... )
    return y;
if ( ...... )
    foo();
    return z;</pre>
<p>这个错误一不小心就犯了，因为人的大脑会相当然地认为缩进的都是一个语句块里的。但是如果原来的代码都加上了大括号，然后把缩进做正常，那么对后面维护的人会是一个非常好的事情。就不会犯我这个低级错误了。就像下面的代码一样，虽然写起来有点罗嗦，但利人利己。</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">if ( ...... ){
    return x;
}
if ( ...... ){
    return y;
}
if ( ...... ){
    return z;
}</pre>
<p>与此类似的代码风格还有如下，你觉得哪个更容易阅读呢？</p>
<ul>
<li>if (!p)    和  if (p == NULL)</li>
</ul>
<ul>
<li>if (p)    和  if (p != NULL)</li>
</ul>
<ul>
<li>if (!bflag)  和 if  (bflag == false)</li>
</ul>
<ul>
<li>if ( CheckSomthing() )  和 if ( CheckSomething() == true )</li>
</ul>
<p>另外还有很多人在switch 语句里用case来做if，也就是说case后面没有break。就像<a href="http://en.wikipedia.org/wiki/Duff's_device" target="_blank">Duff&#8217;s Device</a>一样，再配以goto，代码就写得相当精彩了（这里<a href="https://github.com/agentzh/luajit2/blob/master/src/host/buildvm.c#L395" target="_blank">有个例子</a>）</p>
<p><span style="line-height: 1.5em;">所以说，代码不是炫酷的地方是给别人读的。</span></p>
<p>另外，我在想，为什么苹果的这段代码不写成下面这样的形式？你看，下面这种情况不也很干净吗？</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">
if (  ((err = ReadyHash(&amp;SSLHashSHA1, &amp;hashCtx)) != 0 )
       || ((err = SSLHashSHA1.update(&amp;hashCtx, &amp;clientRandom)) != 0)
       || ((err = SSLHashSHA1.update(&amp;hashCtx, &amp;serverRandom) != 0)
       || ((err = SSLHashSHA1.update(&amp;hashCtx, &amp;signedParams) != 0)
       || ((err = SSLHashSHA1.final(&amp;hashCtx, &amp;hashOut)) != 0)) {

     goto fail;
}
</pre>
<p>其实，还可以做一些代码上的优化，比如，把fail标签里的那些东西写成一个宏，这样就可以去掉goto语句了。</p>
<h5>4）关于goto语句</h5>
<p>关于goto语句，1968年，<a href="http://en.wikipedia.org/wiki/Edsger_Dijkstra">Edsger Dijkstra</a> 投了一篇文章到Communications of the ACM。原本的标题是《A Case Against the Goto Statement》。CACM编辑<a href="http://en.wikipedia.org/wiki/Niklaus_Wirth">Niklaus Wirth</a>灵感来了，把标题改为我们熟知的 《<a href="http://www.u.arizona.edu/~rubinson/copyright_violations/Go_To_Considered_Harmful.html" target="_blank">Go To Statement Considered Harmful</a>》Dijkstra写的内容也是其一贯的犀利语气，文中说：“几年前我就观察到，一个程序员的品质是其程序中goto语句的密度成反比的”，他还说，“后来我发现了为什么goto语句的使用有这么严重的后果，并相信所有高级语言都应该把goto废除掉。”  （<strong>花絮</strong>：因为，这篇文章的出现，计算学界开始用&#8217; <a href="http://en.wikipedia.org/wiki/Considered_harmful">X considered harmful</a> &#8216;当文章标题的风潮，直到<a href="http://meyerweb.com/eric/comment/chech.html">有人终于受不了</a>为止）</p>
<p>为什么goto语句不好呢？Dijkstra说，一个变量代表什么意义要看其上下文。一个程序用N<code></code>记录房间里的人数，在大部分时候，N<code></code>代表的是“目前房间里的人”。但在观察到又有一个人进房间后、把N<code></code>递增的指令前的这段程序区块中，N<code></code>的值代表的是“目前房间里的人数加一”。因此，要正确诠释程序的状态，必须知道程序执行的历史，或着说，知道现在“算到哪”了。</p>
<p>怎么谈“算到哪了”？如果是一直线执行下来的程序，我们只要指到那条语句，说“就是这里”，就可以了。如果是有循环程序，我们可能得说：“现在在循环的这个地方，循环已经执行了第<code>i</code>次”。如果是在函数中，我们可能得说：“现在执行到函数<code>p</code>的这一点；<code>p</code>刚刚被<code>q调用</code>，调用点在一个循环中，这个循环已经执行了<code>i</code>次”。</p>
<p>如果有goto<code>语句了</code>呢？那就麻烦了。因为电脑在执行某个指令前，可能是从程序中许许多多goto<code></code>其中之一跳过来的。要谈某变量的性质也几乎变得不可能了。这就是为什么goto语句问题。</p>
<p>Dijkstra的这篇文章对后面很多程序员有非常深的影响，包括我在内，都觉得Goto语句能不用就不用，虽然，我在十年前的《<a href="http://blog.csdn.net/haoel/article/month/2003/05" target="_blank">编程修养</a>》（这篇文章已经严重过时，某些条目已经漏洞百出）中的<a href="http://blog.csdn.net/haoel/article/details/2876" target="_blank">第23条</a>也说过，我只认为在goto语句只有一种情况可以使用，就是苹果这个bug里的用法。但是我也同意Dijkstra，goto语句能不用就不用了。就苹果的这个问题而言，在更为高级的C++中，<a href="http://en.wikipedia.org/wiki/Resource_Acquisition_Is_Initialization" target="_blank">使用RAII技术</a>，这样的goto语句已经没有什么存在的意义了。</p>
<p>Dijkstra这篇文章后来成为结构化程式论战最有名的文章之一。长达19年之后，Frank Rubin投了一篇文章到CACM,标题为《<a href="http://www.ecn.purdue.edu/ParaMount/papers/rubin87goto.pdf">&#8216; <code></code>Go To Considered Harmful&#8217; Considered Harmful</a> 》Rubin说，「虽然Dijkstra的说法既太学术又缺乏说服力」，却似乎烙到每个程序员的心里了。这样，当有人说“用goto语句来解这题可能会比较好”会被严重鄙视。于是Rubin出了一道这样的题：令<code>X</code>为<code>N * N</code>的整数阵列。如果<code>X</code>的第<code>i</code>行全都是零，请输出<code>i</code>。如果不只一行，输出最小的<code>i</code> .</p>
<p>Rubin找了一些惯用goto和不用goto的程序员来解题，发现用goto的程序又快又清楚。而不用goto通常花了更多的时间，写出很复杂的解答。你觉得呢？ 另外，你会怎么写这题的程序呢？</p>
<p>（<strong>花絮</strong>：以后几个月的CACM热闹死了。编辑收到许多回应，两个月后刊出了其中五篇。文章也包括了《<a href="http://www.ecn.purdue.edu/ParaMount/papers/acm_may87.pdf">&#8220;&#8216;GOTO Considered Harmful&#8217; Considered Harmful&#8221; Considered Harmful?</a> 》）</p>
<p><strong>对于我而言，goto语句的弊远远大于利，在99%的情况下，我是站在反goto这边的</strong>。Java和Python就没有提供Goto语句，原因就是因为goto语句很容易被滥用！</p>
<p><strong>更新：2014年3月5日</strong> &#8211; RedHat 近日也发现个GnuTLS安全问题，与苹果的类似：无法正确检验特定的伪造SSL证书，这个总是会将伪造证书识别为有效证书。虽然Redhat的代码为if加上了花括号，但还是因为没有控制好goto，造成了bug。所以说啊，goto语句的坑是很多。</p>
<ul>
<li>BUG页面：<a href="https://bugzilla.redhat.com/show_bug.cgi?id=1069865" target="_blank">https://bugzilla.redhat.com/show_bug.cgi?id=1069865</a></li>
</ul>
<ul>
<li>相关的Diff: <a href="https://bugzilla.redhat.com/attachment.cgi?id=867911&amp;action=diff" target="_blank">https://bugzilla.redhat.com/attachment.cgi?id=867911&amp;action=diff</a></li>
</ul>
<p>goto语句在写代码的时候也许你会很爽，但是在维护的时候，绝对是一堆坑！redhat的这个patch为原来本来只有一个label的goto又加了另一个label，现在两个label交差goto，继续挖坑……</p>
<h4>总结</h4>
<p>你看，我们不能完全消灭问题，但是，我们可以用下面几个手段来减少问题：</p>
<p style="padding-left: 30px;">1）<strong>尽量在编译上发生错误，而不是在运行时</strong>。</p>
<p style="padding-left: 30px;">2）<strong>代码是让人读的，顺便让机器运行</strong>。不要怕麻烦，好的代码风格，易读的代码会减少很多问题。</p>
<p style="padding-left: 30px;">3）<strong>Code Review是一件很严肃的事情</strong>，但 Code Reivew的前提条件是代码的可读性一定要很好。</p>
<p style="padding-left: 30px;">4）<strong>测试是一件很重要也是很难的事情，尤其是开发人员要非常重视</strong>。</p>
<p style="padding-left: 30px;">5）<strong>不要走飞线，用飞线来解决问题是可耻的！</strong>所以，用goto语句来组织代码的时代过去了，你可以有很多种方式不用goto也可以把代码组织得很好。</p>
<p>最后，我在淘宝过去的一年里，经历过一些P1/P2故障，尤其是去年的8-9月份故障频发的月份，我发现其中有70%的P1/P2故障，就是因为没有code review，没有做好测试，大量地用飞线来解决问题，归根结底就是只重业务结果，对技术没有应有的严谨的态度和敬畏之心。</p>
<p><span style="color: #cc0000;"><strong>正如苹果的这个“goto fail”事件所暗喻的，如果你对技术没有应有的严谨和敬畏之心，你一定会——</strong></span></p>
<p style="text-align: center; font-size: 36px; color: #cc0000; font-family: Georgia,;"><strong>Go To Fail !!!</strong></p>
<p>在这里唠叨这么多，与大家共勉！</p>
<p>（全文完）<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/20845.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/03/rust-social-wide-150x150.jpg" alt="Rust语言的编程范式" width="150" height="150" /></a><a href="https://coolshell.cn/articles/20845.html" class="wp_rp_title">Rust语言的编程范式</a></li><li ><a href="https://coolshell.cn/articles/18360.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2018/05/300x262-150x150.jpg" alt="程序员练级攻略（2018)  与我的专栏" width="150" height="150" /></a><a href="https://coolshell.cn/articles/18360.html" class="wp_rp_title">程序员练级攻略（2018)  与我的专栏</a></li><li ><a href="https://coolshell.cn/articles/18024.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2017/07/api-design-300x278-2-150x150.jpg" alt="API设计原则 &#8211; Qt官网的设计实践总结" width="150" height="150" /></a><a href="https://coolshell.cn/articles/18024.html" class="wp_rp_title">API设计原则 &#8211; Qt官网的设计实践总结</a></li><li ><a href="https://coolshell.cn/articles/12052.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/29.jpg" alt="Leetcode 编程训练" width="150" height="150" /></a><a href="https://coolshell.cn/articles/12052.html" class="wp_rp_title">Leetcode 编程训练</a></li><li ><a href="https://coolshell.cn/articles/12012.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2014/10/edsm-150x150.gif" alt="State Threads 回调终结者" width="150" height="150" /></a><a href="https://coolshell.cn/articles/12012.html" class="wp_rp_title">State Threads 回调终结者</a></li><li ><a href="https://coolshell.cn/articles/11466.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2014/04/c99-150x150.jpg" alt="C语言的整型溢出问题" width="150" height="150" /></a><a href="https://coolshell.cn/articles/11466.html" class="wp_rp_title">C语言的整型溢出问题</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/11112.html">由苹果的低级Bug想到的</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/11112.html/feed</wfw:commentRss>
			<slash:comments>116</slash:comments>
		
		
			</item>
		<item>
		<title>一个“蝇量级” C 语言协程库</title>
		<link>https://coolshell.cn/articles/10975.html</link>
					<comments>https://coolshell.cn/articles/10975.html#comments</comments>
		
		<dc:creator><![CDATA[Leo]]></dc:creator>
		<pubDate>Tue, 28 Jan 2014 02:50:41 +0000</pubDate>
				<category><![CDATA[C/C++语言]]></category>
		<category><![CDATA[程序设计]]></category>
		<category><![CDATA[趣味问题]]></category>
		<category><![CDATA[C++]]></category>
		<category><![CDATA[coroutine]]></category>
		<category><![CDATA[Queue]]></category>
		<category><![CDATA[yield]]></category>
		<category><![CDATA[协程]]></category>
		<guid isPermaLink="false">http://coolshell.cn/?p=10975</guid>

					<description><![CDATA[<p>（感谢网友 @我的上铺叫路遥 投稿） 协程(coroutine)顾名思义就是“协作的例程”（co-operative routines）。跟具有操作系统概念的线...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/10975.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/10975.html">一个“蝇量级” C 语言协程库</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><strong>（感谢网友 </strong><a href="http://weibo.com/fullofbull" target="_blank"><strong>@我的上铺叫路遥</strong></a><strong> 投稿）</strong></p>
<p>协程(coroutine)顾名思义就是“协作的例程”（co-operative routines）。跟具有操作系统概念的线程不一样，协程是在用户空间利用程序语言的语法语义就能实现逻辑上类似多任务的编程技巧。实际上协程的概念比线程还要早，按照 Knuth 的说法<strong>“子例程是协程的特例”</strong>，一个子例程就是一次子函数调用，那么实际上协程就是类函数一样的程序组件，你可以在一个线程里面轻松创建数十万个协程，就像数十万次函数调用一样。只不过子例程只有一个调用入口起始点，返回之后就结束了，而协程入口既可以是起始点，又可以从上一个返回点继续执行，也就是说协程之间可以通过 yield 方式转移执行权，<strong>对称（symmetric）、平级</strong>地调用对方，而不是像例程那样上下级调用关系。当然 Knuth 的“特例”指的是协程也可以模拟例程那样实现上下级调用关系，这就叫<strong>非对称协程</strong>（asymmetric coroutines）。</p>
<h4>基于事件驱动模型</h4>
<p>我们举一个例子来看看一种<strong>对称协程</strong>调用场景，大家最熟悉的“生产者-消费者”事件驱动模型，一个协程负责生产产品并将它们加入队列，另一个负责从队列中取出产品并使用它。为了提高效率，你想一次增加或删除多个产品。伪代码可以是这样的：</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW"># producer coroutine
loop
while queue is not full
  create some new items
  add the items to queue
yield to consumer

# consumer coroutine
loop
while queue is not empty
  remove some items from queue
  use the items
yield to producer</pre>
<p><span id="more-10975"></span></p>
<p>大多数教材上拿这种模型作为多线程的例子，实际上多线程在此的应用还是显得有点“重量级”，由于缺乏 yield 语义，线程之间不得不使用同步机制来避免产生全局资源的竟态，这就不可避免产生了休眠、调度、切换上下文一类的系统开销，而且线程调度还会产生时序上的不确定性。而对于协程来说，“挂起”的概念只不过是转让代码执行权并调用另外的协程，待到转让的协程告一段落后重新得到调用并从挂起点“唤醒”，这种协程间的调用是逻辑上可控的，时序上确定的，可谓一切尽在掌握中。</p>
<p>当今一些具备协程语义的语言，比较重量级的如C#、erlang、golang，以及轻量级的python、lua、javascript、ruby，还有函数式的scala、scheme等。相比之下，作为原生态语言的 C 反而处于尴尬的地位，原因在于 C 依赖于一种叫做<strong>栈帧</strong>的例程调用，例程内部的状态量和返回值都保留在堆栈上，这意味着生产者和消费者相互之间无法实现平级调用，当然你可以改写成把生产者作为主例程然后将产品作为传递参数调用消费者例程，这样的代码写起来费力不讨好而且看起来会很难受，特别当协程数目达到十万数量级，这种写法就过于僵化了。</p>
<p>这就引出了协程的概念，<strong>如果将每个协程的上下文（比如程序计数器）保存在其它地方而不是堆栈上，协程之间相互调用时，被调用的协程只要从堆栈以外的地方恢复上次出让点之前的上下文即可，这有点类似于 CPU 的上下文切换，</strong>遗憾的是似乎只有更底层的汇编语言才能做到这一点。</p>
<p>难道 C 语言只能用多线程吗？幸运的是，C 标准库给我们提供了两种协程调度原语：一种是<a title="http://zh.wikipedia.org/wiki/Setjmp.h" href="http://zh.wikipedia.org/wiki/Setjmp.h" target="_blank"> setjmp/longjmp</a>，另一种是<a title="http://pubs.opengroup.org/onlinepubs/7990989799/xsh/ucontext.h.html" href="http://pubs.opengroup.org/onlinepubs/7990989799/xsh/ucontext.h.html" target="_blank"> ucontext 组件</a>，它们内部（当然是用汇编语言）实现了协程的上下文切换，相较之下前者在应用上会产生相当的不确定性（比如不好封装，具体说明参考联机文档），所以后者应用更广泛一些，网上绝大多数 C 协程库也是基于 ucontext 组件实现的。</p>
<h4>“蝇量级”的协程库</h4>
<p>在此，我来介绍一种“蝇量级”的开源 C 协程库 <a title="http://dunkels.com/adam/pt/" href="http://dunkels.com/adam/pt/" target="_blank">protothreads</a>。这是一个全部用 ANSI C 写成的库，之所以称为“蝇量级”的，就是说，实现已经不能再精简了，几乎就是原语级别。事实上 protothreads 整个库不需要链接加载，因为所有源码都是头文件，类似于 STL 这样不依赖任何第三方库，在任何平台上可移植；总共也就 5 个头文件，有效代码量不足 100 行；API 都是宏定义的，所以不存在调用开销；最后，每个协程的空间开销是 2 个字节（是的，你没有看错，就是一个 short 单位的“栈”！）当然这种精简是要以使用上的局限为代价的，接下来的分析会说明这一点。</p>
<p>先来看看 protothreads 作者，<a title="http://dunkels.com/adam/" href="http://dunkels.com/adam/" target="_blank">Adam Dunkels</a>，一位来自瑞典皇家理工学院的计算机天才帅哥。话说这哥们挺有意思的，写了好多轻量级的作品，都是 BSD 许可证。顺便说一句，轻量级开源软件全世界多如牛毛，可像这位哥们写得如此出名的并不多。比如嵌入式网络操作系统 <a title="http://www.contiki-os.org/" href="http://www.contiki-os.org/" target="_blank">Contiki</a>，国人耳熟能详的 TCP/IP 协议栈 <a title="http://en.wikipedia.org/wiki/UIP_(micro_IP)" href="http://en.wikipedia.org/wiki/UIP_(micro_IP)" target="_blank">uIP</a> 和 <a title="http://savannah.nongnu.org/projects/lwip/" href="http://savannah.nongnu.org/projects/lwip/" target="_blank">lwIP</a> 也是出自其手。上述这些软件都是经过数十年企业级应用的考验，质量之高可想而知。</p>
<p>很多人会好奇如此“蝇量级”的代码究竟是怎么实现的呢？在分析 protothreads 源码之前，我先来给大家补一补 C 语言的基础课;-^)简而言之，这利用了 C 语言特性上的一个“奇技淫巧”，而且这种技巧恐怕连许多具备十年以上经验的 C 程序员老手都不见得知晓。当然这里先要声明我不是推荐大家都这么用，实际上这是以破坏语言的代码规范为代价，在一些严肃的项目工程中需要谨慎对待，除非你想被炒鱿鱼。</p>
<h4>C 语言的“yield 语义”</h4>
<p>下面的教程来自于一位 ARM 工程师、天才黑客 <a title="http://www.chiark.greenend.org.uk/~sgtatham/" href="http://www.chiark.greenend.org.uk/~sgtatham/" target="_blank">Simon Tatham</a>（开源 Telnet/SSH 客户端 <a title="http://www.chiark.greenend.org.uk/~sgtatham/putty/" href="http://www.chiark.greenend.org.uk/~sgtatham/putty/" target="_blank">PuTTY</a> 和汇编器 <a title="http://www.nasm.us/" href="http://www.nasm.us/" target="_blank">NASM</a> 的作者，吐槽一句，PuTTY的源码号称是所有正式项目里最难 hack 的 C，你应该猜到作者是什么语言出身）的博文：<a title="http://www.chiark.greenend.org.uk/~sgtatham/coroutines.html" href="http://www.chiark.greenend.org.uk/~sgtatham/coroutines.html" target="_blank">Coroutines in C</a>。中文译文在<a title="http://www.oschina.net/translate/coroutines-in-c" href="http://www.oschina.net/translate/coroutines-in-c" target="_blank">这里</a>。</p>
<p>我们知道 python 的 yield 语义功能类似于一种迭代生成器，函数会保留上次的调用状态，并在下次调用时会从上个返回点继续执行。用 C 语言来写就像这样：</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">int function(void) {
  int i;
  for (i = 0; i &lt; 10; i++)
    return i;   /* won&#039;t work, but wouldn&#039;t it be nice */
}</pre>
<p>连续对它调用 10 次，它能分别返回 0 到 9。该怎样实现呢？可以利用 goto 语句，如果我们在函数中加入一个状态变量，就可以这样实现：</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">int function(void) {
  static int i, state = 0;
  switch (state) {
    case 0: goto LABEL0;
    case 1: goto LABEL1;
  }
  LABEL0: /* start of function */
  for (i = 0; i &lt; 10; i++) {
    state = 1; /* so we will come back to LABEL1 */
    return i;
    LABEL1:; /* resume control straight after the return */
  }
}</pre>
<p>这个方法是可行的。我们在所有需要 yield 的位置都加上标签：起始位置加一个，还有所有 return 语句之后都加一个。每个标签用数字编号，我们在状态变量中保存这个编号，这样就能在我们下次调用时告诉我们应该跳到哪个标签上。每次返回前，更新状态变量，指向到正确的标签；不论调用多少次，针对状态变量的 switch 语句都能找到我们要跳转到的位置。</p>
<p>但这还是难看得很。最糟糕的部分是所有的标签都需要手工维护，还必须保证函数中的标签和开头 switch 语句中的一致。每次新增一个 return 语句，就必须想一个新的标签名并将其加到 switch 语句中；每次删除 return 语句时，同样也必须删除对应的标签。这使得维护代码的工作量增加了一倍。</p>
<p>仔细想想，其实我们可以不用 switch 语句来决定要跳转到哪里去执行，而是<strong>直接利用 switch 语句本身来实现跳转</strong>：</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">int function(void) {
  static int i, state = 0;
  switch (state) {
    case 0: /* start of function */
    for (i = 0; i &lt; 10; i++) {
      state = 1; /* so we will come back to &quot;case 1&quot; */
      return i;
      case 1:; /* resume control straight after the return */
    }
  }
}</pre>
<p>酷！没想到 switch-case 语句可以这样用，其实说白了 C 语言就是脱胎于汇编语言的，switch-case 跟 if-else 一样，无非就是汇编的条件跳转指令的另类实现而已（这也间接解释了为何汇编程序员经常揶揄 C 语言是“大便一样的代码”）。我们还可以用 __LINE__ 宏使其更加一般化：</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">int function(void) {
  static int i, state = 0;
  switch (state) {
    case 0: /* start of function */
    for (i = 0; i &lt; 10; i++) {
      state = __LINE__ + 2; /* so we will come back to &quot;case __LINE__&quot; */
      return i;
      case __LINE__:; /* resume control straight after the return */
    }
  }
}</pre>
<p>这样一来我们可以用宏提炼出一种范式，封装成组件：</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">#define Begin() static int state=0; switch(state) { case 0:
#define Yield(x) do { state=__LINE__; return x; case __LINE__:; } while (0)
#define End() }
int function(void) {
  static int i;
  Begin();
  for (i = 0; i &lt; 10; i++)
    Yield(i);
  End();
}</pre>
<p>怎么样，看起来像不像发明了一种全新的语言？<strong>实际上我们利用了 switch-case 的分支跳转特性，以及预编译的 __LINE__ 宏，实现了一种隐式状态机，最终实现了“yield 语义”。</strong></p>
<p>还有一个问题，当你欢天喜地地将这种鲜为人知的技巧运用到你的项目中，并成功地拿去向你的上司邀功问赏的时候，你的上司会怎样看待你的代码呢？你的宏定义中大括号没有匹配完整，在代码块中包含了未用到的 case，Begin 和 Yield 宏里面不完整的七拼八凑……你简直就是公司里不遵守编码规范的反面榜样！</p>
<p>别着急，在原文中 Simon Tatham 大牛帮你找到一个坚定的反驳理由，我觉得对程序员来说简直是金玉良言。</p>
<p>将编程规范用在这里是不对的。文章里给出的示例代码不是很长，也不很复杂，即便以状态机的方式改写还是能够看懂的。但是随着代码越来越长，改写的难度将越来越大，改写对直观性造成的损失也变得相当相当大。</p>
<p>想一想，一个函数如果包含这样的小代码块：</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">case STATE1:
/* perform some activity */
if (condition) state = STATE2; else state = STATE3;</pre>
<p>对于看代码的人说，这和包含下面小代码块的函数没有多大区别：</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">LABEL1:
/* perform some activity */
if (condition) goto LABEL2; else goto LABEL3;</pre>
<p>是的，这两个函数的结构在视觉上是一样的，而对于函数中实现的算法，两个函数都一样不利于查看。因为你使用协程的宏而炒你鱿鱼的人，一样会因为你写的函数是由小块的代码和 goto 语句组成而吼着炒了你。只是这次他们没有冤枉你，因为像那样设计的函数会严重扰乱算法的结构。</p>
<p><strong>编程规范的目标就是为了代码清晰。</strong>如果将一些重要的东西，像 switch、return 以及 case 语句，隐藏到起“障眼”作用的宏中，从编程规范的角度讲，可以说你扰乱了程序的语法结构，并且违背了代码清晰这一要求。但是我们这样做是为了突出程序的算法结构，而算法结构恰恰是看代码的人更想了解的。</p>
<p><span style="color: #ff0000;"><strong>任何编程规范，坚持牺牲算法清晰度来换取语法清晰度的，都应该重写。</strong></span>如果你的上司因为使用了这一技巧而解雇你，那么在保安把你往外拖的时候要不断告诉他这一点。</p>
<p>原文作者最后给出了一个 MIT 许可证的 <a title="http://www.chiark.greenend.org.uk/~sgtatham/coroutine.h" href="http://www.chiark.greenend.org.uk/~sgtatham/coroutine.h" target="_blank">coroutine.h</a> 头文件。值得一提的是，正如文中所说，这种协程实现方法有个使用上的局限，就是<strong>协程调度状态的保存依赖于 static 变量，而不是堆栈上的局部变量</strong>，实际上也无法用局部变量（堆栈）来保存状态，这就使得代码不具备可重入性和多线程应用。后来作者补充了一种技巧，就是将局部变量包装成函数参数传入的一个虚构的上下文结构体指针，然后用动态分配的堆来“模拟”堆栈，解决了线程可重入问题。但这样一来反而有损代码清晰，比如所有局部变量都要写成对象成员的引用方式，特别是局部变量很多的时候很麻烦，再比如宏定义 malloc/free 的玩法过于托大，不易控制，搞不好还增加了被炒鱿鱼的风险（只不过这次是你活该）。</p>
<p>我个人认为，既然协程本身是一种单线程的方案，那么我们应该假定应用环境是单线程的，不存在代码重入问题，所以我们可以大胆地使用 static 变量，维持代码的简洁和可读性。事实上<strong>我们也不应该在多线程环境下考虑使用这么简陋的协程</strong>，非要用的话，前面提到 glibc 的 ucontext 组件也是一种可行的替代方案，它提供了一种协程私有堆栈的上下文，当然这种用法在跨线程上也并非没有限制，请仔细阅读联机文档。</p>
<h4>Protothreads的上下文</h4>
<p>感谢 Simon Tatham 的淳淳教诲，接下来我们可以 hack 一下源码了。先来看看实现 protothreads 的数据结构， 实际上它就是协程的<strong>上下文结构体</strong>，用以保存状态变量，相信你很快就明白为何它的“堆栈”只有 2 个字节：</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">struct pt {
  lc_t lc;
}</pre>
<p>里面只有一个 short 类型的变量，实际上它是用来保存上一次出让点的程序计数器。这也映证了协程比线程的灵活之处，就是协程可以是 stackless 的，如果需要实现的功能很单一，比如像生产者-消费者模型那样用来做事件通知，那么实际上协程需要保存的状态变量仅仅是一个程序计数器即可。像 python generator 也是 stackless 的，当然实现一个迭代生成器可能还需要保留上一个迭代值，前面 C 的例子是用 static 变量保存，你也可以设置成员变量添加到上下文结构体里面。如果你真的不确定用协程调度时需要保存多少状态变量，那还是用 ucontext 好了，它的上下文提供了堆栈和信号，但是由用户负责分配资源，详细使用方法见联机文档。。</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">typedef struct ucontext {
  struct ucontext_t *uc_link;
  sigset_t uc_sigmask;
  stack_t uc_stack;
  ...
} ucontext_t;</pre>
<h4>Protothreads的原语和组件</h4>
<p>有点扯远了，回到 protothreads，看看提供的协程“原语”。有两种实现方法，在 ANSI C 下，就是传统的 switch-case 语句：</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">#define LC_INIT（s） s = 0;  // 源码中是有分号的，一个低级 bug，啊哈～
#define LC_RESUME(s) switch (s) { case 0:
#define LC_SET(s) s = __LINE__; case __LINE__:
#define LC_END(s) }
</pre>
<p>但这种“原语”有个难以察觉的缺陷：<strong>就是你无法在 LC_RESUME 和 LC_END （或者包含它们的组件）之间的代码中使用 switch-case语句，因为这会引起外围的 switch 跳转错误！</strong>为此，protothreads 又实现了基于 GNU C 的调度“原语”。在 GNU C 下还有一种语法糖叫做标签指针，就是在一个 label 前面加 &amp;&amp;（不是地址的地址，是 GNU 自定义的符号），可以用 void 指针类型保存，然后 goto 跳转：</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">typedef void * lc_t；
#define LC_INIT(s) s = NULL
#define LC_RESUME(s) \
  do { \
    if (s != NULL) { \
      goto *s; \
    }
  } while (0)
#define LC_CONCAT2(s1, s2) s1##s2
#define LC_CONCAT(s1, s2) LC_CONCAT2(s1, s2)
#define LC_SET(s) \
  do { \
    LC_CONCAT(LC_LABEL, __LINE__): \
    （s） = &amp;&amp;LC_CONCAT(LC_LABEL, __LINE__); \
  } while (0)</pre>
<p>好了，有了前面的基础知识，理解这些“原语”就是小菜一叠，下面看看如何建立“组件”，同时也是 protothreads API，我们先定义四个退出码作为协程的<strong>调度状态机</strong>：</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">#define PT_WAITING 0
#define PT_YIELDED 1
#define PT_EXITED  2
#define PT_ENDED   3</pre>
<p>下面这些 API 可直接在应用程序中调用：</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">/* 初始化一个协程，也即初始化状态变量 */
#define PT_INIT(pt) LC_INIT((pt)-&gt;lc)

/* 声明一个函数，返回值为 char 即退出码，表示函数体内使用了 proto thread，（个人觉得有些多此一举） */
#define PT_THREAD(name_args) char name_args

/* 协程入口点， PT_YIELD_FLAG=0表示出让，=1表示不出让，放在 switch 语句前面，下次调用的时候可以跳转到上次出让点继续执行 */
#define PT_BEGIN(pt) { char PT_YIELD_FLAG = 1; LC_RESUME((pt)-&gt;lc)

/* 协程退出点，至此一个协程算是终止了，清空所有上下文和标志 */
#define PT_END(pt) LC_END((pt)-&gt;lc); PT_YIELD_FLAG = 0; \
                   PT_INIT(pt); return PT_ENDED; }

/* 协程出让点，如果此时协程状态变量 lc 已经变为 __LINE__ 跳转过来的，那么 PT_YIELD_FLAG = 1，表示从出让点继续执行。 */
#define PT_YIELD(pt)        \
  do {            \
    PT_YIELD_FLAG = 0;        \
    LC_SET((pt)-&gt;lc);       \
    if(PT_YIELD_FLAG == 0) {      \
      return PT_YIELDED;      \
    }           \
  } while(0)

/* 附加出让条件 */
#define PT_YIELD_UNTIL(pt, cond)    \
  do {            \
    PT_YIELD_FLAG = 0;        \
    LC_SET((pt)-&gt;lc);       \
    if((PT_YIELD_FLAG == 0) || !(cond)) { \
      return PT_YIELDED;      \
    }           \
  } while(0)

/* 协程阻塞点(blocking),本质上等同于 PT_YIELD_UNTIL，只不过退出码是 PT_WAITING，用来模拟信号量同步 */
#define PT_WAIT_UNTIL(pt, condition)          \
  do {            \
    LC_SET((pt)-&gt;lc);       \
    if(!(condition)) {        \
      return PT_WAITING;      \
    }           \
  } while(0)

/* 同 PT_WAIT_UNTIL 条件反转 */
#define PT_WAIT_WHILE(pt, cond)  PT_WAIT_UNTIL((pt), !(cond))

/* 协程调度，调用协程 f 并检查它的退出码，直到协程终止返回 0，否则返回 1。 */
#define PT_SCHEDULE(f) ((f) &lt; PT_EXITED)

/* 这用于非对称协程，调用者是主协程，pt 是和子协程 thread （可以是多个）关联的上下文句柄，主协程阻塞自己调度子协程，直到所有子协程终止 */
#define PT_WAIT_THREAD(pt, thread) PT_WAIT_WHILE((pt), PT_SCHEDULE(thread))

/* 用于协程嵌套调度，child 是子协程的上下文句柄 */
#define PT_SPAWN(pt, child, thread)   \
  do {            \
    PT_INIT((child));       \
    PT_WAIT_THREAD((pt), (thread));   \
  } while(0)</pre>
<p>暂时介绍这么多，用户还可以根据自己的需求随意扩展组件，比如实现信号量，你会发现脱离了操作系统环境下的信号量竟是如此简单：</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">struct pt_sem {
  unsigned int count;
};

#define PT_SEM_INIT(s, c) (s)-&gt;count = c

#define PT_SEM_WAIT(pt, s)  \
  do {            \
    PT_WAIT_UNTIL(pt, (s)-&gt;count &gt; 0);    \
    --(s)-&gt;count;       \
  } while(0)

#define PT_SEM_SIGNAL(pt, s) ++(s)-&gt;count</pre>
<p>这些应该不需要我多说了吧，呵呵，让我们回到最初例举的生产者-消费者模型，看看protothreads表现怎样。</p>
<h4>Protothreads实战</h4>
<pre data-enlighter-language="c" class="EnlighterJSRAW">#include &quot;pt-sem.h&quot;

#define NUM_ITEMS 32
#define BUFSIZE 8

static struct pt_sem mutex, full, empty;

PT_THREAD(producer(struct pt *pt))
{
  static int produced;

  PT_BEGIN(pt);
  for (produced = 0; produced &lt; NUM_ITEMS; ++produced) {
    PT_SEM_WAIT(pt, &amp;full);
    PT_SEM_WAIT(pt, &amp;mutex);
    add_to_buffer(produce_item());
    PT_SEM_SIGNAL(pt, &amp;mutex);
    PT_SEM_SIGNAL(pt, &amp;empty);
  }
  PT_END(pt);
}

PT_THREAD(consumer(struct pt *pt))
{
  static int consumed;

  PT_BEGIN(pt);
  for (consumed = 0; consumed &lt; NUM_ITEMS; ++consumed) {
    PT_SEM_WAIT(pt, &amp;empty);
    PT_SEM_WAIT(pt, &amp;mutex);
    consume_item(get_from_buffer());
    PT_SEM_SIGNAL(pt, &amp;mutex);
    PT_SEM_SIGNAL(pt, &amp;full);
  }
  PT_END(pt);
}

PT_THREAD(driver_thread(struct pt *pt))
{
  static struct pt pt_producer, pt_consumer;

  PT_BEGIN(pt);
  PT_SEM_INIT(&amp;empty, 0);
  PT_SEM_INIT(&amp;full, BUFSIZE);
  PT_SEM_INIT(&amp;mutex, 1);
  PT_INIT(&amp;pt_producer);
  PT_INIT(&amp;pt_consumer);
  PT_WAIT_THREAD(pt, producer(&amp;pt_producer) &amp; consumer(&amp;pt_consumer));
  PT_END(pt);
}</pre>
<p>源码包中的 example-buffer.c 包含了可运行的完整示例，我就不全部贴了。整体框架就是一个 asymmetric coroutines，包括一个主协程 driver_thread 和两个子协程 producer 和 consumer ，其实不用多说大家也懂的，代码非常清晰直观。我们完全可以通过单线程实现一个简单的事件处理需求，你可以任意添加数十万个协程，几乎不会引起任何额外的系统开销和资源占用。唯一需要留意的地方就是没有一个局部变量，因为 protothreads 是 stackless 的，但这不是问题，首先我们已经假定运行环境是单线程的，其次在一个简化的需求下也用不了多少“局部变量”。如果在协程出让时需要保存一些额外的状态量，像迭代生成器，只要数目和大小都是确定并且可控的话，自行扩展协程上下文结构体即可。</p>
<p>当然这不是说 protothreads 是万能的，它只是贡献了一种模型，你要使用它首先就得学会适应它。下面列举一些 protothreads 的使用限制：</p>
<ul>
<li>由于协程是stackless的，尽量不要使用局部变量，除非该变量对于协程状态是无关紧要的，同理可推，协程所在的代码是不可重入的。</li>
</ul>
<ul>
<li>如果协程使用 switch-case 原语封装的组件，那么禁止在实际应用中使用 switch-case 语句，除非用 GNU C 语法中的标签指针替代。</li>
</ul>
<ul>
<li>一个协程内部可以调用其它例程，比如库函数或系统调用，但必须保证该例程是非阻塞的，否则所在线程内的所有协程都将被阻塞。毕竟线程才是执行的最小单位，协程不过是按“时间片轮度”的例程而已。</li>
</ul>
<p>官网上还例举了更多<a title="http://dunkels.com/adam/pt/examples.html" href="http://dunkels.com/adam/pt/examples.html" target="_blank">实例</a>，都非常实用。另外，一个叫 Craig Graham 的工程师扩展了 pt.h，使得 protothreads 支持 sleep/wake/kill 等操作，文件在此 <a title="http://dunkels.com/adam/download/graham-pt.h" href="http://dunkels.com/adam/download/graham-pt.h" target="_blank">graham-pt.h</a>。</p>
<h4>协程库 DIY 攻略</h4>
<p>看到这里，手养的你是否想迫不及待地 DIY 一个协程组件呢？哪怕很多动态语言本身已经支持了协程语义，很多 C 程序员仍然倾向于自己实现组件，网上很多开源代码底层用的主要还是 glibc 的 ucontext 组件，毕竟提供堆栈的协程组件使用起来更加通用方便。你可以自己写一个调度器，然后模拟线程上下文，再然后……你就能搞出一个跨平台的COS了（笑）。GNU Pth 线程库就是这么实现的，其原作者德国人 <a title="http://engelschall.com/" href="http://engelschall.com/" target="_blank">Ralf S. Engelschall</a> （又是个开源大牛，还写了 <a title="http://engelschall.com/software-artist.php" href="http://engelschall.com/software-artist.php" target="_blank">OpenSSL 等许多作品</a>）就写了一篇<a title="http://xmailserver.org/rse-pmt.pdf" href="http://xmailserver.org/rse-pmt.pdf" target="_blank">论文</a>教大家如何实现一个线程库。另外 protothreads 官网上也有一大堆<a title="http://dunkels.com/adam/pt/links.html" href="http://dunkels.com/adam/pt/links.html" target="_blank">推荐阅读</a>。Have fun！</p>
<p>（全文完）<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/12012.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2014/10/edsm-150x150.gif" alt="State Threads 回调终结者" width="150" height="150" /></a><a href="https://coolshell.cn/articles/12012.html" class="wp_rp_title">State Threads 回调终结者</a></li><li ><a href="https://coolshell.cn/articles/8239.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2012/09/lock_free_bicycle-150x150.jpg" alt="无锁队列的实现" width="150" height="150" /></a><a href="https://coolshell.cn/articles/8239.html" class="wp_rp_title">无锁队列的实现</a></li><li ><a href="https://coolshell.cn/articles/20845.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/03/rust-social-wide-150x150.jpg" alt="Rust语言的编程范式" width="150" height="150" /></a><a href="https://coolshell.cn/articles/20845.html" class="wp_rp_title">Rust语言的编程范式</a></li><li ><a href="https://coolshell.cn/articles/18360.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2018/05/300x262-150x150.jpg" alt="程序员练级攻略（2018)  与我的专栏" width="150" height="150" /></a><a href="https://coolshell.cn/articles/18360.html" class="wp_rp_title">程序员练级攻略（2018)  与我的专栏</a></li><li ><a href="https://coolshell.cn/articles/18024.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2017/07/api-design-300x278-2-150x150.jpg" alt="API设计原则 &#8211; Qt官网的设计实践总结" width="150" height="150" /></a><a href="https://coolshell.cn/articles/18024.html" class="wp_rp_title">API设计原则 &#8211; Qt官网的设计实践总结</a></li><li ><a href="https://coolshell.cn/articles/12052.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/29.jpg" alt="Leetcode 编程训练" width="150" height="150" /></a><a href="https://coolshell.cn/articles/12052.html" class="wp_rp_title">Leetcode 编程训练</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/10975.html">一个“蝇量级” C 语言协程库</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/10975.html/feed</wfw:commentRss>
			<slash:comments>54</slash:comments>
		
		
			</item>
		<item>
		<title>伙伴分配器的一个极简实现</title>
		<link>https://coolshell.cn/articles/10427.html</link>
					<comments>https://coolshell.cn/articles/10427.html#comments</comments>
		
		<dc:creator><![CDATA[Leo]]></dc:creator>
		<pubDate>Wed, 09 Oct 2013 15:10:42 +0000</pubDate>
				<category><![CDATA[C/C++语言]]></category>
		<category><![CDATA[程序设计]]></category>
		<category><![CDATA[趣味问题]]></category>
		<category><![CDATA[Algorithm]]></category>
		<category><![CDATA[Buddy]]></category>
		<category><![CDATA[内存管理]]></category>
		<guid isPermaLink="false">http://coolshell.cn/?p=10427</guid>

					<description><![CDATA[<p>（感谢网友 @我的上铺叫路遥 投稿） 提起buddy system相信很多人不会陌生，它是一种经典的内存分配算法，大名鼎鼎的Linux底层的内存管理用的就是它。...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/10427.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/10427.html">伙伴分配器的一个极简实现</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><strong>（感谢网友 </strong><a href="http://weibo.com/fullofbull" target="_blank"><strong>@我的上铺叫路遥</strong></a><strong> 投稿）</strong></p>
<p>提起buddy system相信很多人不会陌生，它是一种经典的内存分配算法，大名鼎鼎的Linux底层的内存管理用的就是它。这里不探讨内核这么复杂实现，而仅仅是将该算法抽象提取出来，同时给出一份及其简洁的源码实现，以便定制扩展。</p>
<p>伙伴分配的实质就是一种特殊的<strong>“分离适配”</strong>，即将内存按2的幂进行划分，相当于分离出若干个块大小一致的空闲链表，搜索该链表并给出同需求最佳匹配的大小。其优点是快速搜索合并（O(logN)时间复杂度）以及低外部碎片（最佳适配best-fit）；其缺点是内部碎片，因为按2的幂划分块，如果碰上66单位大小，那么必须划分128单位大小的块。但若需求本身就按2的幂分配，比如可以先分配若干个内存池，在其基础上进一步细分就很有吸引力了。</p>
<p>可以在<a href="http://en.wikipedia.org/wiki/Buddy_memory_allocation" target="_blank">维基百科</a>上找到该算法的描述，大体如是：</p>
<p><strong>分配内存：</strong></p>
<p>1.寻找大小合适的内存块（大于等于所需大小并且最接近2的幂，比如需要27，实际分配32）</p>
<p style="padding-left: 30px;">1.如果找到了，分配给应用程序。<br />
2.如果没找到，分出合适的内存块。</p>
<p style="padding-left: 60px;">1.对半分离出高于所需大小的空闲内存块<br />
2.如果分到最低限度，分配这个大小。<br />
3.回溯到步骤1（寻找合适大小的块）<br />
4.重复该步骤直到一个合适的块</p>
<p><span id="more-10427"></span></p>
<p><strong>释放内存：</strong></p>
<p>1.释放该内存块</p>
<p style="padding-left: 30px;">1.寻找相邻的块，看其是否释放了。<br />
2.如果相邻块也释放了，合并这两个块，重复上述步骤直到遇上未释放的相邻块，或者达到最高上限（即所有内存都释放了）。</p>
<p>上面这段文字对你来说可能看起来很费劲，没事，我们看个内存分配和释放的示意图你就知道了：</p>
<p><img decoding="async" loading="lazy" class="aligncenter size-full wp-image-10504" alt="" src="https://coolshell.cn/wp-content/uploads/2013/10/buddy-memory-allocation.jpg" width="598" height="346" srcset="https://coolshell.cn/wp-content/uploads/2013/10/buddy-memory-allocation.jpg 598w, https://coolshell.cn/wp-content/uploads/2013/10/buddy-memory-allocation-300x174.jpg 300w, https://coolshell.cn/wp-content/uploads/2013/10/buddy-memory-allocation-467x270.jpg 467w" sizes="(max-width: 598px) 100vw, 598px" /></p>
<p>上图中，首先我们假设我们一个内存块有1024K，当我们需要给A分配70K内存的时候，</p>
<ol>
<li>我们发现1024K的一半大于70K，然后我们就把1024K的内存分成两半，一半512K。</li>
<li>然后我们发现512K的一半仍然大于70K，于是我们再把512K的内存再分成两半，一半是128K。</li>
<li>此时，我们发现128K的一半小于70K，于是我们就分配为A分配128K的内存。</li>
</ol>
<p>后面的，B，C，D都这样，而释放内存时，则会把相邻的块一步一步地合并起来（合并也必需按分裂的逆操作进行合并）。</p>
<p>我们可以看见，这样的算法，用二叉树这个数据结构来实现再合适不过了。</p>
<p>我在网上分别找到<a href="https://github.com/cloudwu/buddy" target="_blank">cloudwu</a>和<a href="https://github.com/wuwenbin/buddy2">wuwenbin</a>写的两份开源实现和测试用例。实际上后一份是对前一份的精简和优化，本文打算从后一份入手讲解，<strong>因为这份实现真正体现了“极简”二字，追求突破常规的，极致简单的设计。</strong>网友对其评价甚高，甚至可用作教科书标准实现，看完之后回过头来看cloudwu的代码就容易理解了。</p>
<p>分配器的整体思想是，通过一个数组形式的完全二叉树来监控管理内存，二叉树的节点用于标记相应内存块的使用状态，高层节点对应大的块，低层节点对应小的块，在分配和释放中我们就通过这些节点的标记属性来进行块的分离合并。如图所示，假设总大小为16单位的内存，我们就建立一个深度为5的满二叉树，根节点从数组下标[0]开始，监控大小16的块；它的左右孩子节点下标[1~2]，监控大小8的块；第三层节点下标[3~6]监控大小4的块……依此类推。</p>
<p style="text-align: center;"><img decoding="async" loading="lazy" class="aligncenter  wp-image-10502" alt="" src="https://coolshell.cn/wp-content/uploads/2013/10/伙伴分配器.jpg" width="591" height="347" srcset="https://coolshell.cn/wp-content/uploads/2013/10/伙伴分配器.jpg 844w, https://coolshell.cn/wp-content/uploads/2013/10/伙伴分配器-300x176.jpg 300w" sizes="(max-width: 591px) 100vw, 591px" /></p>
<p>在分配阶段，首先要搜索大小适配的块，假设第一次分配3，转换成2的幂是4，我们先要对整个内存进行对半切割，从16切割到4需要两步，那么从下标[0]节点开始深度搜索到下标[3]的节点并将其标记为已分配。第二次再分配3那么就标记下标[4]的节点。第三次分配6，即大小为8，那么搜索下标[2]的节点，因为下标[1]所对应的块被下标[3~4]占用了。</p>
<p>在释放阶段，我们依次释放上述第一次和第二次分配的块，即先释放[3]再释放[4]，当释放下标[4]节点后，我们发现之前释放的[3]是相邻的，于是我们立马将这两个节点进行合并，这样一来下次分配大小8的时候，我们就可以搜索到下标[1]适配了。若进一步释放下标[2]，同[1]合并后整个内存就回归到初始状态。</p>
<p>还是看一下源码实现吧，首先是伙伴分配器的数据结构：</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">struct buddy2 {
  unsigned size;
  unsigned longest[1];
};</pre>
<p>这里的成员size表明管理内存的总单元数目（测试用例中是32），成员longest就是二叉树的节点标记，表明所对应的内存块的空闲单位，<strong>在下文中会分析这是整个算法中最精妙的设计。</strong>此处数组大小为1表明这是可以向后扩展的（注：在GCC环境下你可以写成longest[0]，不占用空间，这里是出于可移植性考虑），我们在分配器初始化的buddy2_new可以看到这种用法。</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">struct buddy2* buddy2_new( int size ) {
  struct buddy2* self;
  unsigned node_size;
  int i;

  if (size &lt; 1 || !IS_POWER_OF_2(size))
    return NULL;

  self = (struct buddy2*)ALLOC( 2 * size * sizeof(unsigned));
  self-&gt;size = size;
  node_size = size * 2;

  for (i = 0; i &lt; 2 * size - 1; ++i) {
    if (IS_POWER_OF_2(i+1))
      node_size /= 2;
    self-&gt;longest[i] = node_size;
  }
  return self;
}</pre>
<p>整个分配器的大小就是满二叉树节点数目，即所需管理内存单元数目的2倍。一个节点对应4个字节，longest记录了节点所对应的的内存块大小。</p>
<p>内存分配的alloc中，入参是分配器指针和需要分配的大小，返回值是内存块索引。alloc函数首先将size调整到2的幂大小，并检查是否超过最大限度。然后进行适配搜索，深度优先遍历，当找到对应节点后，<strong>将其longest标记为0，即分离适配的块出来，</strong>并转换为内存块索引offset返回，依据二叉树排列序号，比如内存总体大小32，我们找到节点下标[8]，内存块对应大小是4，则offset = (8+1)*4-32 = 4，那么分配内存块就从索引4开始往后4个单位。</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">int buddy2_alloc(struct buddy2* self, int size) {
  unsigned index = 0;
  unsigned node_size;
  unsigned offset = 0;

  if (self==NULL)
    return -1;

  if (size &lt;= 0)
    size = 1;
  else if (!IS_POWER_OF_2(size))
    size = fixsize(size);

  if (self-&gt;longest[index] &lt; size)
    return -1;

  for(node_size = self-&gt;size; node_size != size; node_size /= 2 ) {
    if (self-&gt;longest[LEFT_LEAF(index)] &gt;= size)
      index = LEFT_LEAF(index);
    else
      index = RIGHT_LEAF(index);
  }

  self-&gt;longest[index] = 0;
  offset = (index + 1) * node_size - self-&gt;size;

  while (index) {
    index = PARENT(index);
    self-&gt;longest[index] =
      MAX(self-&gt;longest[LEFT_LEAF(index)], self-&gt;longest[RIGHT_LEAF(index)]);
  }

  return offset;
}</pre>
<p>在函数返回之前需要回溯，因为小块内存被占用，大块就不能分配了，比如下标[8]标记为0分离出来，那么其父节点下标[0]、[1]、[3]也需要相应大小的分离。<strong>将它们的longest进行折扣计算，取左右子树较大值，</strong>下标[3]取4，下标[1]取8，下标[0]取16，表明其对应的最大空闲值。</p>
<p>在内存释放的free接口，我们只要传入之前分配的内存地址索引，并确保它是有效值。之后就跟alloc做反向回溯，从最后的节点开始一直往上找到longest为0的节点，即当初分配块所适配的大小和位置。<strong>我们将longest恢复到原来满状态的值。继续向上回溯，检查是否存在合并的块，依据就是左右子树longest的值相加是否等于原空闲块满状态的大小，如果能够合并，就将父节点longest标记为相加的和</strong>（多么简单！）。</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">void buddy2_free(struct buddy2* self, int offset) {
  unsigned node_size, index = 0;
  unsigned left_longest, right_longest;

  assert(self &amp;&amp; offset &gt;= 0 &amp;&amp; offset &lt; size);

  node_size = 1;
  index = offset + self-&gt;size - 1;

  for (; self-&gt;longest[index] ; index = PARENT(index)) {
    node_size *= 2;
    if (index == 0)
      return;
  }

  self-&gt;longest[index] = node_size;

  while (index) {
    index = PARENT(index);
    node_size *= 2;

    left_longest = self-&gt;longest[LEFT_LEAF(index)];
    right_longest = self-&gt;longest[RIGHT_LEAF(index)];

    if (left_longest + right_longest == node_size)
      self-&gt;longest[index] = node_size;
    else
      self-&gt;longest[index] = MAX(left_longest, right_longest);
  }
}</pre>
<p>上面两个成对alloc/free接口的时间复杂度都是O(logN)，保证了程序运行性能。然而这段程序设计的独特之处就在于<strong>使用加权来标记内存空闲状态，而不是一般的有限状态机，实际上longest既可以表示权重又可以表示状态，状态机就毫无必要了，所谓“少即是多”嘛！</strong>反观cloudwu的实现，将节点标记为UNUSED/USED/SPLIT/FULL四个状态机，反而会带来额外的条件判断和管理实现，而且还不如数值那样精确。从逻辑流程上看，wuwenbin的实现简洁明了如同教科书一般，特别是左右子树的走向，内存块的分离合并，块索引到节点下标的转换都是一步到位，不像cloudwu充斥了大量二叉树的深度和长度的间接计算，让代码变得晦涩难读，这些都是longest的功劳。<strong>一个“极简”的设计往往在于你想不到的突破常规思维的地方。</strong></p>
<p>这份代码唯一的缺陷就是longest的大小是4字节，内存消耗大。但<a href="http://blog.codingnow.com/2011/12/buddy_memory_allocation.html" target="_blank">cloudwu的博客</a>上有人提议用logN来保存值，这样就能实现uint8_t大小了，<strong>看，又是一个“极简”的设计！</strong></p>
<p>说实话，很难在网上找到比这更简约更优雅的buddy system实现了——至少在Google上如此。<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/17225.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2015/08/cuckoo-150x150.jpg" alt="Cuckoo Filter：设计与实现" width="150" height="150" /></a><a href="https://coolshell.cn/articles/17225.html" class="wp_rp_title">Cuckoo Filter：设计与实现</a></li><li ><a href="https://coolshell.cn/articles/12052.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/29.jpg" alt="Leetcode 编程训练" width="150" height="150" /></a><a href="https://coolshell.cn/articles/12052.html" class="wp_rp_title">Leetcode 编程训练</a></li><li ><a href="https://coolshell.cn/articles/11847.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2014/08/puzzle-150x150.png" alt="谜题的答案和活动的心得体会" width="150" height="150" /></a><a href="https://coolshell.cn/articles/11847.html" class="wp_rp_title">谜题的答案和活动的心得体会</a></li><li ><a href="https://coolshell.cn/articles/11832.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2014/08/538efefbgw1eiz9cvx78fj20rm0fmdi8-150x150.jpg" alt="【活动】解迷题送礼物" width="150" height="150" /></a><a href="https://coolshell.cn/articles/11832.html" class="wp_rp_title">【活动】解迷题送礼物</a></li><li ><a href="https://coolshell.cn/articles/10590.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2013/10/QR-Code-Overview-150x150.jpeg" alt="二维码的生成细节和原理" width="150" height="150" /></a><a href="https://coolshell.cn/articles/10590.html" class="wp_rp_title">二维码的生成细节和原理</a></li><li ><a href="https://coolshell.cn/articles/9886.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/8.jpg" alt="二叉树迭代器算法" width="150" height="150" /></a><a href="https://coolshell.cn/articles/9886.html" class="wp_rp_title">二叉树迭代器算法</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/10427.html">伙伴分配器的一个极简实现</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/10427.html/feed</wfw:commentRss>
			<slash:comments>55</slash:comments>
		
		
			</item>
		<item>
		<title>C++11的Lambda使用一例：华容道求解</title>
		<link>https://coolshell.cn/articles/10476.html</link>
					<comments>https://coolshell.cn/articles/10476.html#comments</comments>
		
		<dc:creator><![CDATA[Solstice]]></dc:creator>
		<pubDate>Wed, 09 Oct 2013 07:50:21 +0000</pubDate>
				<category><![CDATA[C/C++语言]]></category>
		<category><![CDATA[C++]]></category>
		<category><![CDATA[Lambda]]></category>
		<guid isPermaLink="false">http://coolshell.cn/?p=10476</guid>

					<description><![CDATA[<p>（感谢网友 @bnu_chenshuo 投稿） 华容道是一个有益的智力游戏，游戏规则不再赘述。用计算机求解华容道也是一道不错的编程练习题，为了寻求最少步数，求解...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/10476.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/10476.html">C++11的Lambda使用一例：华容道求解</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><strong>（感谢网友 <a href="http://weibo.com/u/1701018393?source=webim" target="_blank"><img decoding="async" alt="" src="http://tp2.sinaimg.cn/1701018393/50/1297990315/1" /></a><a title="bnu_chenshuo" href="http://weibo.com/u/1701018393?source=webim" target="_blank"> @bnu_chenshuo </a>投稿）</strong></p>
<p style="text-align: center;"><img decoding="async" loading="lazy" class="wp-image-10490 alignright" alt="" src="https://coolshell.cn/wp-content/uploads/2013/10/huarong.png" width="365" height="227" srcset="https://coolshell.cn/wp-content/uploads/2013/10/huarong.png 522w, https://coolshell.cn/wp-content/uploads/2013/10/huarong-300x186.png 300w, https://coolshell.cn/wp-content/uploads/2013/10/huarong-435x270.png 435w" sizes="(max-width: 365px) 100vw, 365px" /></p>
<p>华容道是一个有益的智力游戏，游戏规则不再赘述。用计算机求解华容道也是一道不错的编程练习题，为了寻求最少步数，求解程序一般用广度优先搜索算法。华容道的一种常见开局如图 1 所示。</p>
<p>广度优先搜索算法求解华容道的基本步骤：</p>
<ol>
<li>准备两个“全局变量”，队列 Q 和和集合 S，S 代表“已知局面”。初时 Q 和 S 皆为空。</li>
<li>将初始局面加入队列 Q 的末尾，并将初始局面设为已知。</li>
<li>当队列不为空时，从 Q 的队首取出当前局面 <code>curr</code>。如果队列为空则结束搜索，表明无解。</li>
<li>如果 <code>curr</code> 是最终局面（曹操位于门口，图 2），则结束搜索，否则继续到第 5 步。</li>
<li>考虑 <code>curr</code> 中每个可以移动的棋子，试着上下左右移动一步，得到新局面 <code>next</code>，如果新局面未知（<code>next</code> ∉ S），则把它加入队列 Q，并设为已知。这一步可能产生多个新局面。</li>
<li>回到第2步。</li>
</ol>
<p>其中“局面已知”并不要求每个棋子的位置相同，而是指棋子的投影的形状相同（代码中用 mask 表示），例如交换图 1 中的张飞和赵云并不产生新局面，这一规定可以大大缩小搜索空间。</p>
<p>以上步骤很容易转换为 C++ 代码，这篇文章重点关注的是第 5 步的实现。</p>
<p><span id="more-10476"></span></p>
<pre data-enlighter-language="cpp" class="EnlighterJSRAW">// 第 1 步
std::unordered_set&lt;Mask&gt; seen;
std::deque&lt;State&gt; queue;

// 第 2 步
State initial;
// 填入 initial，略。
queue.push_back(initial);
seen.insert(initial.toMask());

// 第 3 步
while (!queue.empty())
{
  const State curr = queue.front();
  queue.pop_front();

  // 第 4 步
  if (curr.isSolved())
    break;

  // 第 5 步
  for (const State&amp; next : curr.moves())
  {
    auto result = seen.insert(next.toMask());
    if (result.second)
      queue.push_back(next);
  }
}</pre>
<p>在以上原始实现中，<code>curr.move()</code> 将返回一个 <code>std::vector&lt;State&gt;</code> 临时对象。一种节省开销的办法是准备一个 <code>std::vector&lt;State&gt;</code> “涂改变量”，让 <code>curr.move()</code> 反复修改它，比如改成：</p>
<pre data-enlighter-language="cpp" class="EnlighterJSRAW">// 第 1 步新增一个 scratch 变量
std::vector&lt;State&gt; nextMoves;

// 第 3 步
while (!queue.empty())
{
  // ...
  // 第 5 步
  curr.fillMoves(&amp;nextMoves);
  for (const State&amp; next : nextMoves)
  { /* 略 */ }
}</pre>
<p>还有一种彻底不用这个 <code>std::vector&lt;State&gt;</code> 的办法，把一部分逻辑以 lambda 的形式传给 <code>curr.move()</code>，代码的结构基本不变：</p>
<pre data-enlighter-language="cpp" class="EnlighterJSRAW">// 第 3 步
while (!queue.empty())
{
  // ...
  // 第 5 步
  curr.move([&amp;seen, &amp;queue](const State&amp; next) {
    auto result = seen.insert(next.toMask());
    if (result.second)
      queue.push_back(next);
  });
}</pre>
<p>这样一来，主程序的逻辑依然清晰，不必要的开销也降到了最小。</p>
<p>在我最早的实现中，<code>curr.move()</code> 的参数是 <code>const std::function&lt;void(const State&amp;)&gt; &amp;</code>，但是我发现这里每次构造 <code>std::function&lt;void(const State&amp;)&gt;</code> 对象都会分配一次内存，似乎有些不值。因此在现在的实现中 <code>curr.move()</code> 是个函数模板，这样就能自动匹配lambda参数（通常是个 struct 对象），省去了 <code>std::function</code>的内存分配。</p>
<p>本文完整的代码见 <a href="https://github.com/chenshuo/recipes/blob/master/puzzle/huarong.cc">https://github.com/chenshuo/recipes/&#8230;/puzzle/huarong.cc</a>，需用 GCC 4.7 编译，求解图 1 的题目的耗时约几十毫秒。</p>
<p><strong>练习：</strong>修改程序，打印每一步移动棋子的情况。<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/22422.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2023/05/monolith.microservices-150x150.png" alt="是微服务架构不香还是云不香？" width="150" height="150" /></a><a href="https://coolshell.cn/articles/22422.html" class="wp_rp_title">是微服务架构不香还是云不香？</a></li><li ><a href="https://coolshell.cn/articles/20845.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/03/rust-social-wide-150x150.jpg" alt="Rust语言的编程范式" width="150" height="150" /></a><a href="https://coolshell.cn/articles/20845.html" class="wp_rp_title">Rust语言的编程范式</a></li><li ><a href="https://coolshell.cn/articles/18360.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2018/05/300x262-150x150.jpg" alt="程序员练级攻略（2018)  与我的专栏" width="150" height="150" /></a><a href="https://coolshell.cn/articles/18360.html" class="wp_rp_title">程序员练级攻略（2018)  与我的专栏</a></li><li ><a href="https://coolshell.cn/articles/18024.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2017/07/api-design-300x278-2-150x150.jpg" alt="API设计原则 &#8211; Qt官网的设计实践总结" width="150" height="150" /></a><a href="https://coolshell.cn/articles/18024.html" class="wp_rp_title">API设计原则 &#8211; Qt官网的设计实践总结</a></li><li ><a href="https://coolshell.cn/articles/12052.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/29.jpg" alt="Leetcode 编程训练" width="150" height="150" /></a><a href="https://coolshell.cn/articles/12052.html" class="wp_rp_title">Leetcode 编程训练</a></li><li ><a href="https://coolshell.cn/articles/12012.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2014/10/edsm-150x150.gif" alt="State Threads 回调终结者" width="150" height="150" /></a><a href="https://coolshell.cn/articles/12012.html" class="wp_rp_title">State Threads 回调终结者</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/10476.html">C++11的Lambda使用一例：华容道求解</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/10476.html/feed</wfw:commentRss>
			<slash:comments>12</slash:comments>
		
		
			</item>
		<item>
		<title>C++面试中string类的一种正确写法</title>
		<link>https://coolshell.cn/articles/10478.html</link>
					<comments>https://coolshell.cn/articles/10478.html#comments</comments>
		
		<dc:creator><![CDATA[Solstice]]></dc:creator>
		<pubDate>Wed, 09 Oct 2013 07:40:38 +0000</pubDate>
				<category><![CDATA[C/C++语言]]></category>
		<category><![CDATA[C++]]></category>
		<category><![CDATA[面试]]></category>
		<guid isPermaLink="false">http://coolshell.cn/?p=10478</guid>

					<description><![CDATA[<p>（感谢网友 @bnu_chenshuo 投稿） C++ 的一个常见面试题是让你实现一个 String 类，限于时间，不可能要求具备 std::string 的功...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/10478.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/10478.html">C++面试中string类的一种正确写法</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><strong>（感谢网友 <a href="http://weibo.com/u/1701018393?source=webim" target="_blank"><img decoding="async" alt="" src="http://tp2.sinaimg.cn/1701018393/50/1297990315/1" /></a><a title="bnu_chenshuo" href="http://weibo.com/u/1701018393?source=webim" target="_blank"> @bnu_chenshuo </a>投稿）</strong></p>
<p>C++ 的一个常见面试题是让你实现一个 String 类，限于时间，不可能要求具备 std::string 的功能，但至少要求能正确管理资源。具体来说：</p>
<ol>
<li>能像 int 类型那样定义变量，并且支持赋值、复制。</li>
<li>能用作函数的参数类型及返回类型。</li>
<li>能用作标准库容器的元素类型，即 vector/list/deque 的 value_type。（用作 std::map 的 key_type 是更进一步的要求，本文从略）。</li>
</ol>
<p>换言之，你的 String 能让以下代码编译运行通过，并且没有内存方面的错误。</p>
<pre data-enlighter-language="cpp" class="EnlighterJSRAW">void foo(String x)
{
}

void bar(const String&amp; x)
{
}

String baz()
{
  String ret(&quot;world&quot;);
  return ret;
}

int main()
{
  String s0;
  String s1(&quot;hello&quot;);
  String s2(s0);
  String s3 = s1;
  s2 = s1;

  foo(s1);
  bar(s1);
  foo(&quot;temporary&quot;);
  bar(&quot;temporary&quot;);
  String s4 = baz();

  std::vector&lt;String&gt; svec;
  svec.push_back(s0);
  svec.push_back(s1);
  svec.push_back(baz());
  svec.push_back(&quot;good job&quot;);
}</pre>
<p><span id="more-10478"></span>本文给出我认为适合面试的答案，强调正确性及易实现（白板上写也不会错），不强调效率。某种意义上可以说是以时间（运行快慢）换空间（代码简洁）。</p>
<p>首先选择数据成员，最简单的 String 只有一个 char* 成员变量。好处是容易实现，坏处是某些操作的复杂度较高（例如 size() 会是线性时间）。为了面试时写代码不出错，本文设计的 String 只有一个 char* data_成员。而且规定 invariant 如下：一个 valid 的 string 对象的 data_ 保证不为 NULL，data_ 以 <code>'\0'</code> 结尾，以方便配合 C 语言的 str*() 系列函数。</p>
<p>其次决定支持哪些操作，构造、析构、拷贝构造、赋值这几样是肯定要有的（以前合称 big three，现在叫 copy control）。如果钻得深一点，C++11的移动构造和移动赋值也可以有。为了突出重点，本文就不考虑 operator[] 之类的重载了。</p>
<p>这样代码基本上就定型了：</p>
<pre data-enlighter-language="cpp" class="EnlighterJSRAW">#include &lt;utility&gt;
#include &lt;string.h&gt;

class String
{
 public:
  String()
    : data_(new char[1])
  {
    *data_ = &#039;&#092;&#048;&#039;;
  }

  String(const char* str)
    : data_(new char[strlen(str) + 1])
  {
    strcpy(data_, str);
  }

  String(const String&amp; rhs)
    : data_(new char[rhs.size() + 1])
  {
    strcpy(data_, rhs.c_str());
  }
  /* Delegate constructor in C++11
  String(const String&amp; rhs)
    : String(rhs.data_)
  {
  }
  */

  ~String()
  {
    delete[] data_;
  }

  /* Traditional:
  String&amp; operator=(const String&amp; rhs)
  {
    String tmp(rhs);
    swap(tmp);
    return *this;
  }
  */
  String&amp; operator=(String rhs) // yes, pass-by-value
  {
    swap(rhs);
    return *this;
  }

  // C++ 11
  String(String&amp;&amp; rhs)
    : data_(rhs.data_)
  {
    rhs.data_ = nullptr;
  }

  String&amp; operator=(String&amp;&amp; rhs)
  {
    swap(rhs);
    return *this;
  }

  // Accessors

  size_t size() const
  {
    return strlen(data_);
  }

  const char* c_str() const
  {
    return data_;
  }

  void swap(String&amp; rhs)
  {
    std::swap(data_, rhs.data_);
  }

 private:
  char* data_;
};</pre>
<p>注意代码的几个要点：</p>
<ol>
<li>只在构造函数里调用 new char[]，只在析构函数里调用 delete[]。</li>
<li>赋值操作符采用了《C++编程规范》推荐的现代写法。</li>
<li>每个函数都只有一两行代码，没有条件判断。</li>
<li>析构函数不必检查 data_ 是否为 NULL。</li>
<li>构造函数 <code>String(const char* str)</code> 没有检查 str 的合法性，这是一个永无止境的争论话题。这里在初始化列表里就用到了 str，因此在函数体内用 assert() 是无意义的。</li>
</ol>
<p>这恐怕是最简洁的 String 实现了。</p>
<p><strong>练习1</strong>：增加 operator==、operator&lt;、operator[] 等操作符重载。</p>
<p><strong>练习2</strong>：实现一个带 int size_; 成员的版本，以空间换时间。</p>
<p><strong>练习3</strong>：受益于右值引用及移动语意，在 C++11 中对 String 实施直接插入排序的性能比C++98/03要高，试编程验证之。（g++的标准库也用到了此技术。）</p>
<p>陈皓注：同时，大家可以移步看看我的一篇老文《<a href="http://blog.csdn.net/haoel/article/details/1491219" target="_blank">STL中String类的问题</a>》<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/12052.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/29.jpg" alt="Leetcode 编程训练" width="150" height="150" /></a><a href="https://coolshell.cn/articles/12052.html" class="wp_rp_title">Leetcode 编程训练</a></li><li ><a href="https://coolshell.cn/articles/7965.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2012/07/fork01jpg-150x150.jpg" alt="一个fork的面试题" width="150" height="150" /></a><a href="https://coolshell.cn/articles/7965.html" class="wp_rp_title">一个fork的面试题</a></li><li ><a href="https://coolshell.cn/articles/4162.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/0.jpg" alt="又一个有趣的面试题" width="150" height="150" /></a><a href="https://coolshell.cn/articles/4162.html" class="wp_rp_title">又一个有趣的面试题</a></li><li ><a href="https://coolshell.cn/articles/3961.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/21.jpg" alt="“火柴棍式”程序员面试题" width="150" height="150" /></a><a href="https://coolshell.cn/articles/3961.html" class="wp_rp_title">“火柴棍式”程序员面试题</a></li><li ><a href="https://coolshell.cn/articles/3738.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/26.jpg" alt="打印质数的各种算法" width="150" height="150" /></a><a href="https://coolshell.cn/articles/3738.html" class="wp_rp_title">打印质数的各种算法</a></li><li ><a href="https://coolshell.cn/articles/3445.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/14.jpg" alt="输出从1到1000的数" width="150" height="150" /></a><a href="https://coolshell.cn/articles/3445.html" class="wp_rp_title">输出从1到1000的数</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/10478.html">C++面试中string类的一种正确写法</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/10478.html/feed</wfw:commentRss>
			<slash:comments>40</slash:comments>
		
		
			</item>
	</channel>
</rss>
