<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <author>
    <name>Sam Hou</name>
  </author>
  <generator uri="https://hexo.io/">Hexo</generator>
  <icon>https://img.samhou.top/SamHou.png</icon>
  <id>https://blog.samhou.moe/</id>
  <link href="https://blog.samhou.moe/" rel="alternate"/>
  <link href="https://blog.samhou.moe/atom.xml" rel="self"/>
  <rights>All rights reserved 2026, Sam Hou</rights>
  <subtitle>Sleep for a better life.</subtitle>
  <title>SamHou's Blog</title>
  <updated>2026-04-17T11:04:03.000Z</updated>
  <entry>
    <author>
      <name>Sam Hou</name>
    </author>
    <category term="linux教程" scheme="https://blog.samhou.moe/categories/linux-tutorial/"/>
    <category term="mjj" scheme="https://blog.samhou.moe/tags/mjj/"/>
    <category term="vps" scheme="https://blog.samhou.moe/tags/vps/"/>
    <category term="linux" scheme="https://blog.samhou.moe/tags/linux/"/>
    <category term="ssh" scheme="https://blog.samhou.moe/tags/ssh/"/>
    <category term="fail2ban" scheme="https://blog.samhou.moe/tags/fail2ban/"/>
    <category term="ufw" scheme="https://blog.samhou.moe/tags/ufw/"/>
    <content>
      <![CDATA[<p>你是不是一直有这样的问题：</p><ul><li>买了台 VPS，但是默认的镜像用的是 root 感觉一点也不安全</li><li>天天有人扫 22 端口，不小心设置了弱密码，机器被爆破之后被利用，惨遭删机</li><li>新买的 VPS 不知道做什么安全措施好</li></ul><p>如果是，那么这篇文章就是为你准备的，我们将一步一步配置，提升你的 VPS 安全性。</p><p>今天我重装了一台吃灰的 VPS，把整个提升安全性的措施记录在这里，建议收藏一下，以后每次买了新机都可以回来抄作业~</p><p>让我们开始吧！</p><h2 id="连接到-VPS"><a href="#连接到-VPS" class="headerlink" title="连接到 VPS"></a>连接到 VPS</h2><p>这个不多说，检查面板或者邮件，里面有 root 密码。如果没说端口号就是 22。实在连不上开个工单问问商家也行。</p><p>确保你能连上，比如这是我的，一台 Debian 服务器：</p><p><img src="https://img.samhou.top/1776416800430.webp" alt="连接 SSH"></p><div class="note warning flat"><p><strong>dd 脚本</strong></p><p>如果你要用脚本<strong>重装系统</strong>，请现在执行，因为这会<strong>清理所有配置和数据</strong>。</p></div><h2 id="系统更新"><a href="#系统更新" class="headerlink" title="系统更新"></a>系统更新</h2><p>因为你刚部署机器，如果硬盘充足，你可以更新一次系统，这样可以显著减少安全隐患，而且万一不小心被更新搞坏了，还能重新点重装，成本不高：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs bash">apt update  <br>apt upgrade<br></code></pre></td></tr></table></figure><p>注意这是 debian 的 apt 包管理。你可能需要根据系统调整命令。</p><p>然后重启，看看机器能不能启动，ssh 能否连上。如果很不幸系统死了，请重装，然后跳过系统更新这一步。</p><h2 id="探针、测评"><a href="#探针、测评" class="headerlink" title="探针、测评"></a>探针、测评</h2><p>如果需要探针和测评，请现在执行。如果是关键的建站机器，不建议打开远程 ssh，防止面板被黑一锅端。</p><p>此处较为简单，不再提供参考。探针一般有自己的一键安装脚本和配置，仅仅列出 NQ 标准测试的脚本：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">bash &lt;(curl -sL https://run.NodeQuality.com)<br></code></pre></td></tr></table></figure><h2 id="修改机器名称"><a href="#修改机器名称" class="headerlink" title="修改机器名称"></a>修改机器名称</h2><p>买来的机器一般是商家生成的<em>随机名称</em>。改一下，改成更加<strong>有意义</strong>的名称，比如我这台机器在香港，用来跑 copilot cli 做 vibe 的，就起名 <code>hk-copilot-dev-vps</code>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">hostnamectl set-hostname hk-copilot-dev-vps<br></code></pre></td></tr></table></figure><div class="note note flat"><p><strong>Systemd</strong></p><p>上面的命令仅仅适用于基于 systemd 的系统。如果你的系统执行该命令发生了错误，你需要以传统方式修改：<br><code>nano /etc/hostname</code> 打开文件写入新的名称，然后 <code>nano /etc/hosts</code> 修改 127.0.1.1 后面的内容！</p></div><p>改名后重启让它生效：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">reboot<br></code></pre></td></tr></table></figure><p>重启再连上 ssh，可以看到 bash 已经显示出了新的名字，这样就完成了！</p><p><img src="https://img.samhou.top/1776416778704.webp" alt="新的 VPS 主机名"></p><h2 id="改密、添加用户"><a href="#改密、添加用户" class="headerlink" title="改密、添加用户"></a>改密、添加用户</h2><p>先把 root 密码改成自己的。记得设定一个<strong>安全的密码</strong>！你可以用各种密码管理器来生成一些。</p><p>输入 <code>passwd</code> 改密。手滑了也没关系，比如下面的图片不小心按了 enter 创建了空密码，系统会自动提示密码为空，再次输入就行了。</p><p><img src="https://img.samhou.top/1776417268965.webp" alt="改 root 密码"></p><p>接下来我们创建一个<strong>用户</strong>。</p><p>其实如果只有你一个人用，root 也不是不行。但是如果你经常手滑希望加上 sudo 确认一下，或者要用 Homebrew，创建一个新的用户仍然是必要的。</p><p>用 adduser 增加一个用户：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">adduser samhou<br></code></pre></td></tr></table></figure><p>输入用户密码。可以不用提供那堆信息，直接 enter 即可。</p><p><img src="https://img.samhou.top/1776417475870.webp" alt="创建新的用户"></p><h2 id="sudo-和免密码"><a href="#sudo-和免密码" class="headerlink" title="sudo 和免密码"></a>sudo 和免密码</h2><p>现在我的用户没有管理权限，因此我们需要赋予 sudo 的权限，允许用户暂时以 root 执行操作。</p><p>为了防止商家给的镜像太过于精简，首先你得装好 sudo：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">apt install <span class="hljs-built_in">sudo</span><br></code></pre></td></tr></table></figure><p>注意上面的命令适用于使用 apt 的系统。</p><p>我的机器看起来已经有了。</p><p><img src="https://img.samhou.top/1776417583402.webp"></p><p>在常见的 debian 系统中，sudo 是一个用户组，你只要把用户加入这个用户组，即可赋予 sudo 权限：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">usermod -aG <span class="hljs-built_in">sudo</span> samhou<br></code></pre></td></tr></table></figure><p>现在切换到新创建的用户：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">su samhou<br></code></pre></td></tr></table></figure><p>你应该仍然处于 root 文件夹里面，这个文件夹只有默认管理员才能碰。尝试下面的命令：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">ls</span><br><span class="hljs-built_in">sudo</span> <span class="hljs-built_in">ls</span><br></code></pre></td></tr></table></figure><p>如果第一个 ls 显示 <code>Permission denied</code>，第二个要求你输入密码然后输出了文件夹内容，说明你配置正确了！</p><p>（我这里文件夹没有任何内容，所以什么输出也没有）</p><p><img src="https://img.samhou.top/1776418129653.webp" alt="sudo 权限测试"></p><p>由于我们的 VPS 一般都是远程连接的，在 ssh 时已经证明身份了，所以可以关掉 sudo 的密码验证（对，这样的话龙虾、cli 这些东西也能随便玩管理员权限了，不用你再给密码），这需要编辑 sudoers。</p><p>先回退 root 用户防止锁死自己，然后用 visudo 编辑：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">exit</span><br>visudo<br></code></pre></td></tr></table></figure><p>如果你只想要自己免密，可以在最后加上一行：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs text">samhou ALL=(ALL) NOPASSWD:ALL<br></code></pre></td></tr></table></figure><p>记得改成自己创建的用户名。</p><p>如果你想让所有 sudo 都免密，比如你有多个用户，请找到这一行：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs text">%sudo ALL=(ALL:ALL) ALL<br></code></pre></td></tr></table></figure><p>把它改成：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs text">%sudo ALL=(ALL:ALL) NOPASSWD:ALL<br></code></pre></td></tr></table></figure><p><img src="https://img.samhou.top/1776418577675.webp" alt="sudo 配置示例"></p><p>然后保存退出。再切换成目标用户尝试：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs bash">su samhou<br><span class="hljs-built_in">sudo</span> <span class="hljs-built_in">ls</span><br></code></pre></td></tr></table></figure><p>是不是不用密码了呢？如果没有要求输入密码，说明成功了！</p><h2 id="SSH"><a href="#SSH" class="headerlink" title="SSH"></a>SSH</h2><h3 id="密钥登录"><a href="#密钥登录" class="headerlink" title="密钥登录"></a>密钥登录</h3><p>接下来，我们来配置 ssh 登录。首先，在<strong>本地</strong>创建密钥：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">ssh-keygen -t ed25519<br></code></pre></td></tr></table></figure><p>按照提示设置保存位置和密码。此处不再赘述。如果不提供保存位置，默认在你的用户文件夹下的 .ssh 文件夹中，文件以加密算法（这里是 ed25519）命名。密钥有一对，私钥和公钥，你必须保护好私钥，这是你连接到服务器时证明你身份的东西。而公钥需要安装到服务器上，让服务器知道你这个人。</p><p><img src="https://img.samhou.top/1776419093720.webp" alt="密钥文件示例"></p><p>linux 可以直接上传公钥到服务器：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">ssh-copy-id samhou@IP<br></code></pre></td></tr></table></figure><p>记得改成自己用户名和 IP，然后输入刚才创建的用户密码。</p><p>如果你是 windows 没法用上面的命令，请跟我来操作，手动安装密钥。</p><p>首先用你创建的<strong>用户凭据</strong>登录（不要用 root！），然后在你的用户目录执行：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">mkdir</span> .ssh<br><span class="hljs-built_in">cd</span> .ssh<br>nano authorized_keys<br></code></pre></td></tr></table></figure><p>然后把你在上一步创建的公钥粘贴进去（公钥就是后缀带有 .pub 的文件内容）。</p><p>保存。然后返回用户目录，改一下文件归属和权限（必须的，否则会被认为不安全而被拒绝）：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">cd</span> ~<br><span class="hljs-built_in">sudo</span> <span class="hljs-built_in">chown</span> -R samhou:samhou .ssh<br><span class="hljs-built_in">sudo</span> <span class="hljs-built_in">chmod</span> 700 .ssh<br><span class="hljs-built_in">sudo</span> <span class="hljs-built_in">chmod</span> 600 .ssh/<br><span class="hljs-built_in">sudo</span> <span class="hljs-built_in">chmod</span> 600 .ssh/authorized_keys<br></code></pre></td></tr></table></figure><p>这些命令应该没有任何输出。如果报错说明你肯定哪里出问题了。</p><p>现在在本地用密钥登录试试。如果没有要求输入密码或要求输入密码解锁密钥，说明一切正常！</p><div class="note warning flat"><p><strong>确保能登录</strong></p><p>请确保你能用密钥登录，再继续执行后续操作！否则你会把自己锁在外面……如果你自己搜索解决方案后仍然无法解决问题，请寻求技术大佬的帮助。</p></div><h3 id="端口和登录控制"><a href="#端口和登录控制" class="headerlink" title="端口和登录控制"></a>端口和登录控制</h3><p>端口和登录认证控制都是由 <code>/etc/ssh/sshd_config</code> 文件控制的。</p><p>先打开这个文件（记得用 sudo 权限）</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">sudo</span> nano /etc/ssh/sshd_config<br></code></pre></td></tr></table></figure><p>接下来，我们来改一下端口。</p><p>找到这个 Port 这一行，取消注释，然后修改端口，随便改一个 22 除外的，别让别人轻易猜到：</p><p><img src="https://img.samhou.top/1776422908902.webp" alt="11451 端口"></p><p>然后禁用 root 登录，找到 <code>PermitRootLogin</code> 改成 no：</p><p><img src="https://img.samhou.top/1776423023418.webp" alt="拒绝 root 登录"></p><p>最后关闭密码登录（这样仅仅允许密钥登录），把 <code>PasswordAuthentication</code> 改成 no，然后去除 <code>PermitEmptyPasswords no</code> 的注释：</p><p><img src="https://img.samhou.top/1776423108157.webp" alt="关闭密码登录"></p><p>保存文件。然后重启 ssh 服务：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">sudo</span> systemctl restart ssh<br></code></pre></td></tr></table></figure><p>如果不行，试试另外一种：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">sudo</span> systemctl restart sshd<br></code></pre></td></tr></table></figure><p>现在用新的端口登录，尝试一下。如果你的主机提供商有非常高级的面板式防火墙，记得去打开你要的端口，防止连不上~</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">ssh samhou@IP -p 11451<br></code></pre></td></tr></table></figure><p><code>-p</code> 表示端口，<strong>不要在 IP 后面直接写冒号</strong>，这是不对的！</p><p>节省篇幅，此处不再展示相同的登录成功页面。</p><h2 id="防火墙"><a href="#防火墙" class="headerlink" title="防火墙"></a>防火墙</h2><p>如果你的主机商家比较菜，一般不会提供面板防火墙。这时候，你可以用 ufw 管理你的 iptables，快速配置防火墙。</p><p>先装上：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">sudo</span> apt install ufw<br></code></pre></td></tr></table></figure><p>然后，允许你想要的端口，比如你建站想要的 web 服务端口，以及刚才我们改好的 11451 ssh 端口：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">sudo</span> ufw allow 80<br><span class="hljs-built_in">sudo</span> ufw allow 443<br><span class="hljs-built_in">sudo</span> ufw allow 11451<br></code></pre></td></tr></table></figure><p><strong>确定你的 ssh 端口包含了之后</strong>（别把自己锁外面了），你就可以激活防火墙了：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">sudo</span> ufw <span class="hljs-built_in">enable</span><br></code></pre></td></tr></table></figure><p>然后就是再次连接 ssh，看看能不能连上。如果你很不幸把自己拦截在外面了，可以用商家提供的 vnc 登录（不算 ssh 所以直接输入密码登入即可），关掉防火墙再连接。</p><h2 id="fail2ban"><a href="#fail2ban" class="headerlink" title="fail2ban"></a>fail2ban</h2><p>虽然改完端口仅仅允许密钥登录，理论上已经非常安全了，但是我还是建议你加上 fail2ban 来提升安全性，它会自动封禁扫描你机器的 IP。</p><p>先装好：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">sudo</span> apt install fail2ban<br></code></pre></td></tr></table></figure><p>然后把模板配置复制一份给自定义配置，打开新建的自定义配置文件：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">sudo</span> <span class="hljs-built_in">cp</span> /etc/fail2ban/jail.conf /etc/fail2ban/jail.local<br><span class="hljs-built_in">sudo</span> nano /etc/fail2ban/jail.local<br></code></pre></td></tr></table></figure><p>忽略开头的注释，向下滚动，你首先会看到 <code>[DEFAULT]</code> 这个标签，然后是一堆提示。继续向下滚动，根据注释，你可以配置你想要的 ban 策略，比如 10 分钟里面认证失败 5 次直接封禁 24 小时：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs text">bantime = 24h<br>findtime = 10m<br>maxretry = 5<br></code></pre></td></tr></table></figure><p><img src="https://img.samhou.top/1776425361076.webp" alt="配置策略"></p><p>继续向下滚动，找到 <code>[sshd]</code>，添加 <code>enabled = true</code>，然后改一下端口 port，改成目标端口：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs text">[sshd]<br>enabled = true<br>port = 11451<br></code></pre></td></tr></table></figure><p>如果你的系统是基于 systemd 的（systemctl 命令可用），那么把 backend 改成下面这样：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs text">backend = systemd<br></code></pre></td></tr></table></figure><p>如果你的系统不用 systemd，还是传统的行为，会把访问日志放在文件里，那么把 logpath 改成这样：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs text">logpath = /var/log/auth.log<br></code></pre></td></tr></table></figure><p>请注意，不同系统上面的路径可能不一样。请自行查询你的系统使用的认证日志的路径，切忌直接复制。</p><p>下面是我的系统使用的配置，debian 12 是基于 systemd 的系统：</p><p><img src="https://img.samhou.top/1776425903666.webp" alt="示例配置"></p><p>保存，重启 fail2ban，并检查状态：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">sudo</span> systemctl restart fail2ban<br><span class="hljs-built_in">sudo</span> systemctl status fail2ban<br></code></pre></td></tr></table></figure><p>如果显示绿的 active (running)，说明你的配置完全正确！如果出错了，不妨问问 AI 或者寻求技术大佬的帮助。</p><p><img src="https://img.samhou.top/1776425987293.webp" alt="正在运行的 fail2ban"></p><p>过一段时间再来看，如果你的 VPS 被扫到了，下面的命令应该会显示被封禁的 IP：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">sudo</span> fail2ban-client status<br></code></pre></td></tr></table></figure><p>至此，你的 VPS 已经相当安全了，本文也就到此为止，你可以继续部署你想要的服务，然后睡个好觉了！</p><div class="note warning flat"><p><strong>别忘了防火墙！</strong></p><p>你在刚才操作的过程中可能用上了 ufw。如果需要开放端口，请执行 ufw allow 命令，别把自己和正常用户拦在外面了……</p></div>]]>
    </content>
    <id>https://blog.samhou.moe/safe-vps/</id>
    <link href="https://blog.samhou.moe/safe-vps/"/>
    <published>2026-04-17T11:04:03.000Z</published>
    <summary>本文详述了 VPS 从零开始的安全配置，包括更新系统、修改主机名、创建新用户并赋予 sudo 权限。重点介绍了 SSH 密钥登录、禁用 root 登录及密码登录，调整 SSH 端口。同时配置 UFW 防火墙以开放必要端口，并安装 Fail2ban 自动封禁恶意 IP，全面提升 VPS 安全性。</summary>
    <title>从零开始配置 VPS —— 主机名、用户组和远程权限安全实践</title>
    <updated>2026-04-17T11:04:03.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Sam Hou</name>
    </author>
    <category term="C++教程" scheme="https://blog.samhou.moe/categories/cpp-tutorial/"/>
    <category term="cpp" scheme="https://blog.samhou.moe/tags/cpp/"/>
    <category term="class" scheme="https://blog.samhou.moe/tags/class/"/>
    <category term="preprocessing" scheme="https://blog.samhou.moe/tags/preprocessing/"/>
    <content>
      <![CDATA[<p><a href="https://blog.samhou.moe/cpp-function-pointer-type/">上一节</a>，我们讲了函数指针。现在，我们来仔细聊聊类这个东西。</p><p>在 C++ 中，类是一种自定义的抽象数据结构，允许你把一组相关的数据存储到一起。什么叫<strong>抽象</strong>数据结构呢？其实就是给一个东西建模，让单个数据<em>代表</em>这个东西。</p><p>有了大体的概念，让我们来看看类到底是怎么玩的。</p><h2 id="自定义数据类型"><a href="#自定义数据类型" class="headerlink" title="自定义数据类型"></a>自定义数据类型</h2><p>先来一段简单的代码：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-keyword">struct</span> <span class="hljs-title class_">Person</span> &#123;<br>    string name;<br>    <span class="hljs-type">int</span> age;<br>&#125;;<br><span class="hljs-function"><span class="hljs-type">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span> </span>&#123;<br>    Person user;<br>    user.name = <span class="hljs-string">&quot;SamHou&quot;</span>;<br>    user.age = <span class="hljs-number">114514</span>;<br>    cout &lt;&lt; user.name &lt;&lt; <span class="hljs-string">&#x27; &#x27;</span> &lt;&lt; user.age &lt;&lt; endl;<br>    <span class="hljs-comment">// SamHou 114514</span><br>&#125;<br></code></pre></td></tr></table></figure><p>我们在此使用了 struct 关键字，来定义属于自己的类。<code>Person</code> 是这个类的<strong>名字</strong>，包含在名字后面大括号中的，称为类的<strong>成员</strong>。大括号外需要一个分号，表示这个类的结尾。</p><p>现在来看 main 函数。</p><p>首先，我们创建了一个 Person 类的<strong>实例</strong>（也叫<strong>对象</strong>），它的名字叫做 user。</p><p>那什么叫创建实例呢？你可以这么理解：类是一种<strong>蓝图</strong>，规定单个数据类型中，<strong>应该有</strong>哪些成员。当我们创建实例的时候，就是根据这个蓝图建了一个“房子”（也就是存在于内存中的独立对象）。</p><p>每个对象可以用自己的名字找到，也因此可以根据名字访问对象的成员。因此<strong>修改不同对象的成员</strong>，并<strong>不会影响其它对象</strong>（即使它们是由同一个蓝图建立的）。</p><p>在之前的文章中，我们已经用了很久这个 <code>.</code> 了。它表示的是，访问一个类的成员，称之为<strong>成员运算符</strong>。</p><p>因此，上面的代码表示，我们把 name 成员改成了 “SamHou” 这个字符串，然后把 age 改成了 114514；然后用同样的方法，再输出。</p><p>要注意的一点是，任何东西（包括类）都要遵循<a href="https://blog.samhou.moe/cpp-return-scope-life/#%E4%BD%9C%E7%94%A8%E5%9F%9F%E5%92%8C%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F">名字作用域和对象生命周期规则</a>。如果你忘了，可以复习一下。</p><p>基本用法很简单，不是吗？但我们得仔细再深入挖一下。</p><p>比如，这时候你可能就有个疑问了——创建一个类的实例的时候，里面的成员会怎样初始化呢？换句话说，那个 user 的 name 和 age 默认是什么呢？</p><p>这就要来讨论下初始化的问题了。</p><h2 id="类成员的初始化"><a href="#类成员的初始化" class="headerlink" title="类成员的初始化"></a>类成员的初始化</h2><h3 id="默认初始化"><a href="#默认初始化" class="headerlink" title="默认初始化"></a>默认初始化</h3><p>如果你<strong>什么都不添加</strong>，那么类会执行默认初始化。在这个过程中，<strong>每个成员</strong>都会执行其<em>自身</em>的<strong>默认初始化</strong>。</p><p>我们回看上面的程序：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-keyword">struct</span> <span class="hljs-title class_">Person</span> &#123;<br>    string name;<br>    <span class="hljs-type">int</span> age;<br>&#125;;<br></code></pre></td></tr></table></figure><p>string 的默认初始化会创建空字符串。而 int 类型的默认初始化，则会变成一个不可预测的随机值（未定义的值）。</p><p>也就是说——你或许根本不知道会发生什么。如果你的类里面有指针类型，那就更麻烦了，未定义的指针谁也不知道指向了什么，这种行为是非常危险的。</p><p>因此，我们先来说说最简单的初始化——<em>类内初始值</em>。</p><h3 id="类内初始值"><a href="#类内初始值" class="headerlink" title="类内初始值"></a>类内初始值</h3><p>类内初始值，和给变量初始化的语法就是一样的：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-keyword">struct</span> <span class="hljs-title class_">Person</span> &#123;<br>    string name = <span class="hljs-string">&quot;Example name&quot;</span>;<br>    <span class="hljs-type">int</span> age = <span class="hljs-number">114</span>;<br>&#125;;<br><span class="hljs-function"><span class="hljs-type">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span> </span>&#123;<br>    Person user;<br>    cout &lt;&lt; user.name &lt;&lt; <span class="hljs-string">&#x27; &#x27;</span> &lt;&lt; user.age &lt;&lt; endl;<br>    <span class="hljs-comment">// Example name 114</span><br>&#125;<br></code></pre></td></tr></table></figure><p>我们通过直接写 &#x3D; 的方式，给每个类的成员赋了初始值。</p><p>在上面的代码中，我们创建实例之后，并没有对这个实例做任何修改。因此，输出的就是默认值。</p><p>但是，有时候一行也太局限了，有没有一种更加灵活的方式，允许我们进行更加自定义的操作呢？</p><p>当然是有的——这叫做<strong>构造函数</strong>。</p><h3 id="构造函数"><a href="#构造函数" class="headerlink" title="构造函数"></a>构造函数</h3><p>顾名思义，构造函数就是类根据蓝图，创建实例时，<strong>执行的初始化函数</strong>。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-keyword">struct</span> <span class="hljs-title class_">Person</span> &#123;<br>    string name = <span class="hljs-string">&quot;Example name&quot;</span>;<br>    <span class="hljs-type">int</span> age = <span class="hljs-number">114</span>;<br>    <span class="hljs-type">int</span> passKey;<br>    <span class="hljs-built_in">Person</span>(string n, <span class="hljs-type">int</span> a, <span class="hljs-type">int</span> password) &#123;<br>        name = n;<br>        age = a;<br>        passKey = age + password;<br>    &#125;<br>&#125;;<br></code></pre></td></tr></table></figure><p>构造函数是一个和<strong>类同名</strong>的<strong>无返回值</strong>函数，接受自定义的参数，然后执行自定义操作。可以直接在构造函数内，通过名字访问类的成员，不需要使用成员运算符（构造函数的作用域里也没有这个实例的名字）。</p><p><em>实际上，这种访问类成员的行为和 this 指针有关。将在下一篇详细解读。</em></p><p>在上面的代码中，我们接受了 n a password 三个值，然后把 n a 赋值给成员，再根据 age 和 password 生成 passKey。（注意：<strong>这是一个构造函数示例。千万不要理解为任何密码生成方法</strong>）</p><p>定义好构造函数后，创建实例时怎么调用呢？来看看吧：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-function"><span class="hljs-type">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span> </span>&#123;<br>    <span class="hljs-function">Person <span class="hljs-title">user</span><span class="hljs-params">(<span class="hljs-string">&quot;SamHou&quot;</span>, <span class="hljs-number">114514</span>, <span class="hljs-number">233</span>)</span></span>;<br>    cout &lt;&lt; user.name &lt;&lt; <span class="hljs-string">&#x27; &#x27;</span> &lt;&lt; user.age &lt;&lt; endl;<br>    <span class="hljs-comment">// SamHou 114514</span><br>    cout &lt;&lt; user.passKey &lt;&lt; endl;<br>    <span class="hljs-comment">// 114747</span><br>&#125;<br></code></pre></td></tr></table></figure><p>Great! 直接在名字后面带上参数列表，即可调用构造函数！</p><p>提示：<strong>实际上，初始化和在函数体内赋值属于不同的操作。在下一小节内，我们会详细阐述这个问题。先留个基本印象。</strong></p><p>现在让我们回到构造函数。我们不禁在想：有那么多参数通常不需要执行自定义操作，而是直接赋值给类的成员，有没有什么更加简便的方法，来执行初始化呢？</p><p>C++ 设计者早就考虑到了。下面是一个等价的定义：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-keyword">struct</span> <span class="hljs-title class_">Person</span> &#123;<br>    string name = <span class="hljs-string">&quot;Example name&quot;</span>;<br>    <span class="hljs-type">int</span> age = <span class="hljs-number">114</span>;<br>    <span class="hljs-type">int</span> passKey;<br>    <span class="hljs-built_in">Person</span>(string n, <span class="hljs-type">int</span> a, <span class="hljs-type">int</span> password): <span class="hljs-built_in">name</span>(n), <span class="hljs-built_in">age</span>(a) &#123;<br>        passKey = age + password;<br>    &#125;<br>&#125;;<br></code></pre></td></tr></table></figure><p>注意到了吗？我们直接在参数列表后，块之前用冒号加入了一个新的列表。这个列表通过初始化类成员的方式，为成员赋初始值，这叫作<strong>构造函数初始化列表</strong>。</p><p>另外也要注意优先级的问题。</p><p>永远记住，构造函数中的赋值操作优先级，高于构造函数初始化列表，最后才是类内初始值。为什么？你别急，看看下面就知道了。</p><h3 id="合成的默认构造函数、初始化列表、重载"><a href="#合成的默认构造函数、初始化列表、重载" class="headerlink" title="合成的默认构造函数、初始化列表、重载"></a>合成的默认构造函数、初始化列表、重载</h3><p>刚才我们讨论了自定义构造函数的情况。那么问题来了——如果构造函数不存在，那么会发生什么呢？</p><p>嗯，你肯定已经知道了，我们上面也提到过：会首先根据类内初始值。如果类内初始值不存在，那么就会对每个成员，执行默认初始化。</p><p>表象是这样的，但是探究一下本质。实际上，C++ 在没有构造函数的情况下，生成的是一个合成的<strong>默认构造函数</strong>。</p><p>这个函数的行为你也很清楚了，上面的“表象”刚刚说过。</p><p>正如其他函数一样，构造函数也可以重载，因此我们在自定义构造函数之外，用 default 可以把默认构造函数“找回来”：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-keyword">struct</span> <span class="hljs-title class_">myClass</span>&#123;<br>    <span class="hljs-built_in">myClass</span>() = <span class="hljs-keyword">default</span>; <span class="hljs-comment">// 执行默认行为，也就是允许 myClass mc;</span><br>    <span class="hljs-built_in">myClass</span>(<span class="hljs-type">int</span> a,...)...<br>&#125;<br></code></pre></td></tr></table></figure><p>好的，现在回到自定义构造函数。刚才我们提到了优先级问题，现在来看看构造函数初始化列表到底怎么工作的：</p><ol><li>如果构造函数初始化列表存在，那么忽略<em>对应的</em><strong>类内初始值</strong>，转而采用初始化列表对成员进行<em>初始化</em>。</li><li>执行完此类初始化操作<strong>后</strong>，开始执行<strong>构造函数体</strong>。</li></ol><p>哦，现在你知道问题所在了——<strong>构造函数初始化列表</strong>和<strong>类内初始值</strong>都属于<strong>初始化操作</strong>。但是函数体内的呢？是赋值操作。既然有这么个“初始化 - 赋值”的运行顺序，自然就表现出这种优先级了（后执行的，永远会取代旧的初始化）。</p><p>构造函数中的<strong>赋值</strong>操作 &gt; 构造函数<strong>初始化列表</strong> &gt; 类内<strong>初始值</strong>。</p><p>聪明的读者一定发现了，<em>初始化</em>和<em>赋值</em>这两个概念我们非常熟悉对吧？我们在 <a href="https://blog.samhou.moe/cpp-pointer-const-guide/">const 限定符与指针</a>这一节中，重点提过它们的区别，可以复习一下。</p><p>之所以提 const，是因为一个非常重要的事情。</p><p>如果是顶层 const，则一旦初始化就不能改变。因此，这一类成员<strong>仅仅可以通过构造函数初始化列表或类内初始值</strong>进行初始化，不能在构造函数里面再赋新的值。</p><h2 id="模块化开发"><a href="#模块化开发" class="headerlink" title="模块化开发"></a>模块化开发</h2><h3 id="头文件和源文件"><a href="#头文件和源文件" class="headerlink" title="头文件和源文件"></a>头文件和源文件</h3><p>在此之前，我们所创建的所有类，都是把函数体写在类内的。但实际上，C++ 更加推荐的一种方式，是把一个类的函数<em>声明</em>放在<strong>头文件</strong>里面，把函数<em>定义</em>放在<strong>源文件</strong>里面。</p><p>我们在<a href="https://blog.samhou.moe/cpp-func-arg-guide/#%E5%A3%B0%E6%98%8E%E5%92%8C%E5%AE%9A%E4%B9%89">函数与参数传递</a>这一节，曾经讲过声明和定义的区别，以及 <code>#include</code> 头文件即可直接使用的好处。</p><p>现在我们写的程序越来越长，于是不得不进行<em>模块化设计</em>（把负责不同工作的代码放到<strong>不同文件</strong>中），因此有必要说清楚以下概念的区别：</p><ul><li>头文件：用于<strong>声明</strong>。类的所有成员声明应该放在这里，包括数据成员和函数成员</li><li>源文件：用于<strong>定义</strong>。必须实现声明中的所有函数成员，否则在调用时就会爆炸</li><li>它们一般是独立的文件。</li><li>C++ <code>#include</code> 头文件后，即可直接使用</li><li>如果不把声明放在头文件里面，非常容易导致重复定义（声明可以多次、但是<strong>定义只能一次</strong>，下面会详细解释问题的根源）</li></ul><p>来看例子，上面的程序如果进行模块化——</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-comment">// Person.h</span><br><span class="hljs-meta">#<span class="hljs-keyword">ifndef</span> PERSON_INCLUDED</span><br><span class="hljs-meta">#<span class="hljs-keyword">define</span> PERSON_INCLUDED</span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span><span class="hljs-string">&lt;string&gt;</span></span><br><span class="hljs-keyword">struct</span> <span class="hljs-title class_">Person</span> &#123;<br>    std::string name = <span class="hljs-string">&quot;Example name&quot;</span>;<br>    <span class="hljs-type">int</span> age = <span class="hljs-number">114</span>;<br>    <span class="hljs-type">int</span> passKey;<br><br>    <span class="hljs-built_in">Person</span>(std::string n, <span class="hljs-type">int</span> a, <span class="hljs-type">int</span> password);<br>&#125;;<br><span class="hljs-meta">#<span class="hljs-keyword">endif</span> <span class="hljs-comment">// PERSON_INCLUDED</span></span><br></code></pre></td></tr></table></figure><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-comment">// Person.cpp</span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&quot;Person.h&quot;</span></span><br><br>Person::<span class="hljs-built_in">Person</span>(std::string n, <span class="hljs-type">int</span> a, <span class="hljs-type">int</span> password) <span class="hljs-comment">// 先记住这个 :: 我们会详细解释这是什么</span><br>    : <span class="hljs-built_in">name</span>(n), <span class="hljs-built_in">age</span>(a) &#123;<br>    passKey = age + password;<br>&#125;<br><br></code></pre></td></tr></table></figure><p>Main 函数不发生改变：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-comment">// main.cpp</span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&lt;iostream&gt;</span></span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&lt;vector&gt;</span></span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&quot;Person.h&quot;</span></span><br><span class="hljs-keyword">using</span> <span class="hljs-keyword">namespace</span> std;<br><span class="hljs-function"><span class="hljs-type">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span> </span>&#123;<br>    <span class="hljs-function">Person <span class="hljs-title">user</span><span class="hljs-params">(<span class="hljs-string">&quot;SamHou&quot;</span>, <span class="hljs-number">114514</span>, <span class="hljs-number">233</span>)</span></span>;<br>    cout &lt;&lt; user.name &lt;&lt; <span class="hljs-string">&#x27; &#x27;</span> &lt;&lt; user.age &lt;&lt; endl;<br>    cout &lt;&lt; user.passKey &lt;&lt; endl;<br>&#125;<br><br></code></pre></td></tr></table></figure><p>看到了吗？我们只在主程序中 include 了头文件，声明就已经完成了。C++ 编译了定义，然后自动拼接起来——因此不需要 include 源文件。</p><p>要注意的点是，数据成员和类内初始值应该放在头文件里面，构造函数的声明在头文件中不要包含初始化列表，而是要放在源文件定义中。</p><p>要想深入了解编译的细节，我们要知道代码真正变成程序的过程中到底发生了什么。实际上，有三个重要阶段：预处理、分离编译、链接，让我们从代码出发，一步步讲清楚。</p><h3 id="预处理和分离式编译"><a href="#预处理和分离式编译" class="headerlink" title="预处理和分离式编译"></a>预处理和分离式编译</h3><p>你可能有个大疑问，这个 <code>#include</code> 到底是做什么的？为什么加上了，头文件里面的声明就能用了？</p><p>这就要提到编译之前的<strong>预处理 (preprocessing)</strong> 了。当你看到这个神奇的 # 号的时候，说明它是一个预处理命令，发生在实际的编译之前。</p><p>而 <code>#include</code> 这个预处理命令，就是把<strong>紧随其后</strong>的头文件，直接<strong>复制</strong>到这个源代码文件中。也就是说，编译只对处理过后的 .cpp 源代码生效，而头文件早已经被复制进去了。</p><p>啥意思？你的代码在你执行编译之前，其实变成了这个样子：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-comment">// Person.cpp</span><br><br><span class="hljs-comment">// #include &quot;Person.h&quot; 现在不存在了！</span><br><span class="hljs-comment">// 内容被复制过来了！</span><br><span class="hljs-meta">#<span class="hljs-keyword">ifndef</span> PERSON_INCLUDED</span><br><span class="hljs-meta">#<span class="hljs-keyword">define</span> PERSON_INCLUDED</span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span><span class="hljs-string">&lt;string&gt;</span></span><br><span class="hljs-keyword">struct</span> <span class="hljs-title class_">Person</span> &#123;<br>    std::string name = <span class="hljs-string">&quot;Example name&quot;</span>;<br>    <span class="hljs-type">int</span> age = <span class="hljs-number">114</span>;<br>    <span class="hljs-type">int</span> passKey;<br><br>    <span class="hljs-built_in">Person</span>(std::string n, <span class="hljs-type">int</span> a, <span class="hljs-type">int</span> password);<br>&#125;;<br><span class="hljs-meta">#<span class="hljs-keyword">endif</span> <span class="hljs-comment">// PERSON_INCLUDED</span></span><br>Person::<span class="hljs-built_in">Person</span>(std::string n, <span class="hljs-type">int</span> a, <span class="hljs-type">int</span> password) <span class="hljs-comment">// 先记住这个 :: 我们会详细解释这是什么</span><br>    : <span class="hljs-built_in">name</span>(n), <span class="hljs-built_in">age</span>(a) &#123;<br>    passKey = age + password;<br>&#125;<br></code></pre></td></tr></table></figure><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-comment">// main.cpp</span><br><span class="hljs-comment">// #include &lt;iostream&gt; 没错，这玩意也要复制</span><br>...<br><span class="hljs-comment">// #include &lt;vector&gt; 还有这个</span><br>...<br><span class="hljs-comment">// #include &quot;Person.h&quot;</span><br><span class="hljs-comment">// 内容被复制过来了！</span><br><span class="hljs-meta">#<span class="hljs-keyword">ifndef</span> PERSON_INCLUDED</span><br><span class="hljs-meta">#<span class="hljs-keyword">define</span> PERSON_INCLUDED</span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span><span class="hljs-string">&lt;string&gt;</span></span><br><span class="hljs-keyword">struct</span> <span class="hljs-title class_">Person</span> &#123;<br>    std::string name = <span class="hljs-string">&quot;Example name&quot;</span>;<br>    <span class="hljs-type">int</span> age = <span class="hljs-number">114</span>;<br>    <span class="hljs-type">int</span> passKey;<br><br>    <span class="hljs-built_in">Person</span>(std::string n, <span class="hljs-type">int</span> a, <span class="hljs-type">int</span> password);<br>&#125;;<br><span class="hljs-meta">#<span class="hljs-keyword">endif</span> <span class="hljs-comment">// PERSON_INCLUDED</span></span><br><span class="hljs-keyword">using</span> <span class="hljs-keyword">namespace</span> std;<br><span class="hljs-function"><span class="hljs-type">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span> </span>&#123;<br>    <span class="hljs-function">Person <span class="hljs-title">user</span><span class="hljs-params">(<span class="hljs-string">&quot;SamHou&quot;</span>, <span class="hljs-number">114514</span>, <span class="hljs-number">233</span>)</span></span>;<br>    cout &lt;&lt; user.name &lt;&lt; <span class="hljs-string">&#x27; &#x27;</span> &lt;&lt; user.age &lt;&lt; endl;<br>    cout &lt;&lt; user.passKey &lt;&lt; endl;<br>&#125;<br></code></pre></td></tr></table></figure><p>哦非常的厉害，<code>.h</code> 直接消失了！现在我们假装自己是编译器，分别编译这两个文件（没错，这就叫作<em>分离式编译</em>，每个文件分别编译然后链接起来变成程序）：</p><ul><li>编译 <code>Person.cpp</code><ul><li>声明 Person，包括其成员。</li><li>用作用域运算符 <code>::</code>（下方介绍），定义 Person 的成员</li></ul></li><li>编译 <code>main.cpp</code><ul><li>声明 Person，包括其成员</li><li>因为声明了，就可以直接使用，这在编译阶段没有任何问题</li></ul></li></ul><p>现在进行链接，把这些文件拼起来：</p><ul><li>找到了 Person 的声明。对应的构造函数确实只在 <code>Person.cpp</code> 里面定义了一次，没问题！</li></ul><p>了解了 <code>#include</code> 和分离式编译，现在我们来解答上面的问题——到底为啥不能把定义写进头文件？</p><h3 id="重复定义问题"><a href="#重复定义问题" class="headerlink" title="重复定义问题"></a>重复定义问题</h3><p>来看看这个例子（只是个让你明白为啥不能这么写的小例子，别太关注具体的内容）：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-comment">// add.h</span><br><span class="hljs-meta">#<span class="hljs-keyword">ifndef</span> ADD_INCLUDED</span><br><span class="hljs-meta">#<span class="hljs-keyword">define</span> ADD_INCLUDED</span><br><span class="hljs-function"><span class="hljs-type">int</span> <span class="hljs-title">add</span><span class="hljs-params">(<span class="hljs-type">int</span> a, <span class="hljs-type">int</span> b)</span></span><br><span class="hljs-function"></span>&#123;<br>    <span class="hljs-keyword">return</span> a + b;<br>&#125;<br><span class="hljs-meta">#<span class="hljs-keyword">endif</span> <span class="hljs-comment">// ADD_INCLUDED</span></span><br></code></pre></td></tr></table></figure><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-comment">// main.cpp</span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&lt;iostream&gt;</span></span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&quot;add.h&quot;</span></span><br><span class="hljs-keyword">using</span> <span class="hljs-keyword">namespace</span> std;<br><span class="hljs-function"><span class="hljs-type">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span><br><span class="hljs-function"></span>&#123;<br>    cout &lt;&lt; <span class="hljs-built_in">add</span>(<span class="hljs-number">1</span>, <span class="hljs-number">2</span>) &lt;&lt; endl;<br>&#125;<br></code></pre></td></tr></table></figure><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-comment">// myMath.cpp</span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&quot;add.h&quot;</span></span><br><span class="hljs-function"><span class="hljs-type">int</span> <span class="hljs-title">sum</span><span class="hljs-params">(<span class="hljs-type">int</span> a, <span class="hljs-type">int</span> b, <span class="hljs-type">int</span> c)</span></span><br><span class="hljs-function"></span>&#123;<br>    <span class="hljs-keyword">return</span> <span class="hljs-built_in">add</span>(a, b) + c;<br>&#125;<br></code></pre></td></tr></table></figure><p>试着编译一下——</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs text">D:\code\Cpp\test\add.h|3|multiple definition of `add(int, int)&#x27;; obj\Debug\main.o:D:/code/Cpp/test/add.h:3: first defined here|<br></code></pre></td></tr></table></figure><p>果不其然，炸了。这个错误出现在链接阶段，因为经过预处理，编译阶段变成了这样：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-comment">// main.cpp</span><br><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&lt;iostream&gt;</span></span><br><span class="hljs-meta">#<span class="hljs-keyword">ifndef</span> ADD_INCLUDED</span><br><span class="hljs-meta">#<span class="hljs-keyword">define</span> ADD_INCLUDED</span><br><span class="hljs-function"><span class="hljs-type">int</span> <span class="hljs-title">add</span><span class="hljs-params">(<span class="hljs-type">int</span> a, <span class="hljs-type">int</span> b)</span></span><br><span class="hljs-function"></span>&#123;<br>    <span class="hljs-keyword">return</span> a + b;<br>&#125;<br><span class="hljs-meta">#<span class="hljs-keyword">endif</span> <span class="hljs-comment">// ADD_INCLUDED</span></span><br><span class="hljs-keyword">using</span> <span class="hljs-keyword">namespace</span> std;<br><span class="hljs-function"><span class="hljs-type">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span><br><span class="hljs-function"></span>&#123;<br>    cout &lt;&lt; <span class="hljs-built_in">add</span>(<span class="hljs-number">1</span>, <span class="hljs-number">2</span>) &lt;&lt; endl;<br>&#125;<br></code></pre></td></tr></table></figure><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-comment">// myMath.cpp</span><br><span class="hljs-meta">#<span class="hljs-keyword">ifndef</span> ADD_INCLUDED</span><br><span class="hljs-meta">#<span class="hljs-keyword">define</span> ADD_INCLUDED</span><br><span class="hljs-function"><span class="hljs-type">int</span> <span class="hljs-title">add</span><span class="hljs-params">(<span class="hljs-type">int</span> a, <span class="hljs-type">int</span> b)</span></span><br><span class="hljs-function"></span>&#123;<br>    <span class="hljs-keyword">return</span> a + b;<br>&#125;<br><span class="hljs-meta">#<span class="hljs-keyword">endif</span> <span class="hljs-comment">// ADD_INCLUDED</span></span><br><span class="hljs-function"><span class="hljs-type">int</span> <span class="hljs-title">sum</span><span class="hljs-params">(<span class="hljs-type">int</span> a, <span class="hljs-type">int</span> b, <span class="hljs-type">int</span> c)</span></span><br><span class="hljs-function"></span>&#123;<br>    <span class="hljs-keyword">return</span> <span class="hljs-built_in">add</span>(a, b) + c;<br>&#125;<br></code></pre></td></tr></table></figure><p>没有任何问题，但是当链接到一起的时候，问题来了——</p><p><strong>在两个源文件里面，都有 add 这个函数的定义！</strong></p><p>C++ 就不允许多次定义，所以直接报错了。</p><p>从上面的内容，我们可以得出下面的结论：</p><p><strong>如果你把定义写在头文件里，一旦多个源文件引用了这个头文件，那么相当于重复定义，这是会直接报错的。</strong></p><p>因此，请确保你在头文件里面只写声明！</p><p>一个小提示：你是不是在想：“为什么这里不拿上面的类来做例子呢”？这是因为如果直接在类内定义函数，那么这个函数就会默认变成 <code>inline</code> 的。inline 的函数是一个<em>特例</em>，它<strong>允许重复定义，不过这些定义必须全部相同</strong>。</p><h3 id="作用域运算符"><a href="#作用域运算符" class="headerlink" title="作用域运算符"></a>作用域运算符</h3><p>等等，上面还有一个十分神奇的符号：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs cpp">Person::<span class="hljs-built_in">Person</span>(std::string n, <span class="hljs-type">int</span> a, <span class="hljs-type">int</span> password)<br></code></pre></td></tr></table></figure><p>这是个啥？它叫作<strong>作用域运算符</strong>。让我们回忆一下之前的<a href="https://blog.samhou.moe/cpp-return-scope-life/?highlight=%E4%BD%9C%E7%94%A8#%E5%90%8D%E5%AD%97%E7%9A%84%E4%BD%9C%E7%94%A8%E5%9F%9F">作用域</a>一个小节中的内容……</p><blockquote><p>让名字具有意义的地方，就叫做作用域（起作用的地方）。<br>在 C++ 中，名字的作用域，一般是一对花括号，也就是块（或者整个程序）。<br>一旦在作用域中声明（不是定义）一个名字，那么它就在该声明语句到作用域末尾有效。</p></blockquote><p> 哦，这下清楚了——我们在类的外面写函数的定义，那么处于类的定义域之外，当然就找不到对应的名字了。</p><p>作用域运算符，能够指定<strong>名字所在作用域</strong>。Person 这个类后面的大括号组成了块，属于独立的作用域。因此 <code>Person::Person()</code> 的含义，就是去 Person 这个名字对应的作用域里面，找一个名字为 Person 的函数。</p><p>我们来画个图：</p><p><img src="https://img.samhou.top/1774100618142.webp" alt="作用域示例"></p><p>Person 这个类所在的有效区间是最大的，以红色框标识。因此，<code>Person::</code> 可以找到 Person 这个类。黄色框标识 Person 类内的一个作用域，<code>Person::</code> 把作用域限定在了这里面。然后，在黄色框中查找 <code>Person</code> 这个名字，找到对应的构造函数 <code>Person()</code>（蓝色框是它的有效区间，这里用不着，只是给你标出来而已）。</p><p>也就是说，这个 :: 可以转换作用域，让目标名字被我们所找到，在类外定义类的成员函数时，是必须的！</p><p>现在，你对类的工作方式和分离式编译已经有了一些基本了解了。下一节，我们将继续探索类中的一个重要概念，this 指针。然后进入面向对象的一个特征——封装，你将会学习对你的类进行访问控制。同样是通俗易懂的语言，下一篇再见！</p>]]>
    </content>
    <id>https://blog.samhou.moe/cpp-class-compile-guide/</id>
    <link href="https://blog.samhou.moe/cpp-class-compile-guide/"/>
    <published>2026-03-21T13:03:18.000Z</published>
    <summary>本文以通俗语言讲解了C++类的基本概念、成员初始化方法、构造函数、模块化开发、预处理器、分离式编译及作用域运算符的用法，让初学者能轻松理解并避免常见错误，为后续学习面向对象打下基础。</summary>
    <title>奶奶都能看懂的 C++ —— 类、初始化、预处理和分离式编译</title>
    <updated>2026-03-21T13:03:18.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Sam Hou</name>
    </author>
    <category term="安全与加密" scheme="https://blog.samhou.moe/categories/safety-encryption/"/>
    <category term="cloudflare" scheme="https://blog.samhou.moe/tags/cloudflare/"/>
    <category term="cdn" scheme="https://blog.samhou.moe/tags/cdn/"/>
    <category term="waf" scheme="https://blog.samhou.moe/tags/waf/"/>
    <category term="nginx" scheme="https://blog.samhou.moe/tags/nginx/"/>
    <category term="ssl" scheme="https://blog.samhou.moe/tags/ssl/"/>
    <category term="mtls" scheme="https://blog.samhou.moe/tags/mtls/"/>
    <content>
      <![CDATA[<p>套 Cloudflare，应该是各位站长的标准操作了吧。</p><div class="note warning flat"><p><strong>注意 SSL 配置</strong></p><p>本文的前提是 Cloudflare 开启 SSL 严格&#x2F;严格（完全）。否则无效。</p></div><h2 id="泄露-IP-和域名"><a href="#泄露-IP-和域名" class="headerlink" title="泄露 IP 和域名"></a>泄露 IP 和域名</h2><p>当你建了个站，把你的域名解析到服务器，打开小黄云，你有没有想过——你怎么知道请求是不是 CF 传递过来的呢？</p><p>众所周知，你的 Nginx 反向代理会根据请求的 Host 头判断目标服务，比如下面的这个 server 配置就来自我的某台服务器：</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs nginx"><span class="hljs-section">server</span> &#123;<br>    <span class="hljs-attribute">listen</span> <span class="hljs-number">443</span> ssl;<br>    <span class="hljs-attribute">listen</span> [::]:<span class="hljs-number">443</span> ssl;<br>    ... <span class="hljs-comment"># ssl</span><br>    <span class="hljs-attribute">server_name</span> status.samhou.top; <span class="hljs-comment"># Host</span><br>    <span class="hljs-section">location</span> / &#123;<br>        <span class="hljs-attribute">proxy_pass</span> http://uptime;<br>        ...<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>也就是说，只要有人知道了你的服务域名+你的服务器 IP，就可以给你的源站发送请求，绕过 cloudflare 直接获取内容。</p><p>这就麻烦了——你可能配了一堆 WAF，又是速率限制又是区域锁定，结果源站泄露，直接变得跟摆设一样。</p><p>不信？你试试下面的 curl 命令：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">curl -k https://ip -H <span class="hljs-string">&quot;Host: example.com&quot;</span><br></code></pre></td></tr></table></figure><p>替换成你的源站 IP 和你的域名试试执行。</p><p><img src="https://img.samhou.top/1773150190381.webp" alt="html 全都出来了"></p><p>那怎么让源站验证你的请求确实来自 Cloudflare 的边缘节点呢？别急，Cloudflare 给你藏了一个非常隐秘的选项。</p><h2 id="经过身份验证的源服务器拉取"><a href="#经过身份验证的源服务器拉取" class="headerlink" title="经过身份验证的源服务器拉取"></a>经过身份验证的源服务器拉取</h2><p>先选中域名，然后导航到 SSL&#x2F;TLS - 源服务器 - 经过身份验证的源服务器拉取，打开这个：</p><p><img src="https://img.samhou.top/1773150463554.webp" alt="经过验证的源服务器拉取"></p><p>这个选项会让 Cloudflare 向源服务器发送自己的证书。</p><p>什么意思呢？</p><p>大家都知道，源站有 SSL 证书来验证自己的身份（SSL 完全严格模式），那么客户端当然也可以有证书来验证自己的身份（这称之为 mTLS 技术）。在我们的情景下，源站是服务端，客户端是 Cloudflare 的边缘节点。</p><p>打开这个选项后，边缘节点将向源站发送 Cloudflare 的证书。源站验证证书，确认来自 Cloudflare 之后，放行。否则，直接拒绝访问。</p><p>我知道大家都用 Nginx，没错，它可以在源站充当身份认证的功能，只需两步——</p><p>首先，下载 CF 的证书（官方文档里面给的地址）：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">wget https://developers.cloudflare.com/ssl/static/authenticated_origin_pull_ca.pem<br></code></pre></td></tr></table></figure><p>然后，把它重命名为 crt 后缀：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">mv</span> ./authenticated_origin_pull_ca.pem ./authenticated_origin_pull_ca.crt<br></code></pre></td></tr></table></figure><p>然后，配置你的 Nginx，根据你的证书保存位置加上两行（此处以放在全局 http 块中为例子。你也可以放在 server 块，为单个域名启用）：</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs nginx"><span class="hljs-section">http</span> &#123;<br>    ...<br>    <span class="hljs-attribute">ssl_client_certificate</span> /etc/nginx/authenticated_origin_pull_ca.crt;<br>    <span class="hljs-attribute">ssl_verify_client</span> <span class="hljs-literal">on</span>;<br>    ...<br>    <span class="hljs-attribute">include</span> /etc/nginx/conf.d/<span class="hljs-regexp">*.conf</span>;<br><br>&#125;<br></code></pre></td></tr></table></figure><p>现在重载 Nginx：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">sudo</span> service nginx reload<br></code></pre></td></tr></table></figure><p>好了，访问你的域名，应该仍然可以正常访问。再试一次，用刚才的 curl 命令绕过 CF 直接访问，你就会很欣喜地发现一个 400 错误，提示没有提供客户端证书：</p><p><img src="https://img.samhou.top/1773151302799.webp" alt="错误"></p><p>现在你的源站防护等级升高了一个档次。</p><p>其实原理很简单：就是源站要求客户端提供证书，然后对证书进行了一次验证而已，和普通的 SSL 只是方向相反而已。</p><div class="note danger flat"><p><strong>Cloudflare 内部威胁</strong></p><p>由于使用了 Cloudflare 的证书，所以只能验证请求来自 CF 的边缘节点，但不能验证请求是你的账号所发出的。也就是说，如果别人通过某种方法让 CF 以你的 Host 回源你的服务器，就会绕过此限制。要解决这个问题，只能自定义上传证书，这不在本文的探讨范围内。</p></div>]]>
    </content>
    <id>https://blog.samhou.moe/cf-authenticated-origin-pulls/</id>
    <link href="https://blog.samhou.moe/cf-authenticated-origin-pulls/"/>
    <published>2026-03-10T14:04:13.000Z</published>
    <summary>本文详细介绍了如何通过 &quot;Authenticated Origin Pulls&quot; 功能验证请求是否来自 Cloudflare 边缘节点，从而提升源站安全性。通过启用 mTLS 技术，使用 Cloudflare 提供的客户端证书配置 Nginx 实现验证，有效防止源站 IP 泄露与绕过攻击。</summary>
    <title>让你验证请求来自 CF —— Authenticated Origin Pulls 攻略</title>
    <updated>2026-03-10T14:04:13.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Sam Hou</name>
    </author>
    <category term="杂谈" scheme="https://blog.samhou.moe/categories/talking/"/>
    <category term="mjj" scheme="https://blog.samhou.moe/tags/mjj/"/>
    <category term="pt" scheme="https://blog.samhou.moe/tags/pt/"/>
    <category term="domain" scheme="https://blog.samhou.moe/tags/domain/"/>
    <category term="art" scheme="https://blog.samhou.moe/tags/art/"/>
    <category term="vps" scheme="https://blog.samhou.moe/tags/vps/"/>
    <content>
      <![CDATA[<p>距离上一篇水星冲浪日志已经过去一个月多了。因此，第二篇日志新鲜出炉~在这一个月多一点的时间里，网上冲浪和技术又发生了许多可以写的内容，甚至让我有些难以抉择了。</p><p>在开始之前，我们先来看看上一篇日志的评论区——收到了一条留言：</p><blockquote><p>关注一下你的装机，来自RSS订阅用户。</p></blockquote><p>嗯，这位读者可能要失望了，上一期组装好机器之后，作为做种机器了（见下方内容），<strong>目前暂时没有继续装机的打算</strong>。下一次装机，预计是等到家里云硬盘出现问题的时候了——不过估计不会太久，等到硬盘和内存价格降下来之后，我希望能<strong>自己组一台 NAS</strong>。毕竟，生命不息，折腾不止。</p><p>留言读完，事不宜迟，让我们开始吧！</p><h2 id="PT"><a href="#PT" class="headerlink" title="PT"></a>PT</h2><p>作为一个追求极致的看番观众，我经常会需要下载老番和 BD 版本（至少也要 1080p 高清）的动漫资源。</p><p>之前都是 bt 下载，然而发现大批吸血客户端吸血，此外还有一堆死种。然后在论坛里面知道了 PT 这个东西，正好有很多 vps 和两条联通宽带+大盘家里云，就开始玩 PT 了。</p><p>PT 对你的分享率是有要求的，保种更是可以获得魔力值换各种各样的东西。</p><p>于是趁着过年 PT 站开始注册，尝试入了几个 PT 站。</p><p>摸索一段时间后，我发现 PT 的正确打法：</p><ul><li>先看规则和常见问题</li><li>规则不允许盒子（vps 等国外大带宽公网设备）？那就用家里宽带下热门种子和免费种子</li><li>先刷上传。刷完上传再保种，下你自己想要的保，这样满足下载和魔力要求</li><li>可以认领，这样能获得很多魔力值</li></ul><p>不过，毕竟我也是个 P 龄才一个月的新人，PT 站也都不允许公开宣传它们，所以此处就略过不多说啦（当然有大佬看到此文想发药，也可以联系我）。</p><p>关于 PT 嘛，我个人是非常喜欢这种互利共赢的做法的——你把资源分发给别人，别人也给你，在 bt 的基础上，确保了所有人都在好好做贡献。显然，这是个比 peerbanhelper 反吸血更好的方法，毕竟所有数据都会被记录下来，你可以安心地做贡献，而不是害怕被吸血。</p><p>再转向下一个话题之前，先来说说——</p><h3 id="机器的归宿"><a href="#机器的归宿" class="headerlink" title="机器的归宿"></a>机器的归宿</h3><p>上一期，我提到过自己装了一台机器。作为一台备用服务器，我把它设置在了异地——另一条宽带所在的地区。虽然盘非常小，但是够用了，只是用来保点 PT 种子，很不错。一个 PT 站要活下去，确保种子活着很重要，因此保种+认领是可以获得魔力值的，非常划算。</p><h2 id="换域名"><a href="#换域名" class="headerlink" title="换域名"></a>换域名</h2><p>接下来就是博客的一件大事：</p><p>top 这个后缀总感觉不太舒服，而且听论坛大佬说对搜索引擎收录有影响，所以我决定——换成 moe 后缀！</p><p>moe 后缀其实早就注册了——小说站和一些项目用的就是 moe。现在只是需要把 top 迁移过来即可。</p><p>首先创建 301 跳转，指示已经永久移动，这个简单，vercel 甚至可以一键自动完成这个操作。至于带域名的链接的话，可以批量VSCode 替换，非常方便。</p><p>然后就是搜索引擎的迁移了。</p><p>Search Console 简单，直接申请换域名，自动检查通过即可。</p><p>Bing 没找到入口，于是给客服发了工单。客服说重定向了会自动建立索引，于是我就等了几天，还真是流量都回来了，看来这个过程确实很少需要人工介入。</p><p>最后也是最难的一步，通知友链更改。当然由于自动 301 重定向，不改也没事，一个个通知会浪费自己和对方的时间，所以就有点摆烂了，只通知了友链下方评论区，以及“开往”、博友圈、虚拟备案这种可以自主更改的朋友和实体。</p><p>如果有看到这篇文章的朋友，麻烦去更新一下友链信息哦，毕竟友链已经加了不少了。RSS 订阅也需要修改域名，只要把 top 改成 moe 即可。</p><p>更改过程非常丝滑，而且由于只是要改那些需要搜索引擎收录的站点，所以评论系统和 umami 数据分析不需要修改。大大减少了工作量，好评。</p><h2 id="约稿"><a href="#约稿" class="headerlink" title="约稿"></a>约稿</h2><p>接下来聊聊博客美术。</p><p>记得上一篇，我们提到了<a href="https://blog.samhou.moe/aqua-surf-1/#%E7%9C%8B%E6%9D%BF%E5%A8%98">看板娘的形象</a>。现在有点钱了，就找了一位老师约稿画了出来。作为一个理工男，想象力实在有限，我甚至害怕画师会因为我的设计太烂而拒绝。但是当我仔细构思过形象，然后把企划挂到平台上之后，很快就有老师来接稿了，于是就生成了这么个结果：</p><p><img src="https://img.samhou.top/seimouhornia.webp" alt="S 酱立绘（请尊重博主和画师的设计，勿复制&#x2F;转载&#x2F;喂给 AI）"></p><p>这个形象，以及修正后的设定，可在<a href="https://blog.samhou.moe/character/">设定集</a>查看。</p><p>看板娘的形象确定下来之后，接下来就有以下的设想：</p><ul><li>根据形象再约稿一个头像</li><li>把看板娘应用到某些博客文章中去</li><li>继续约稿完善博客的美术设计</li></ul><p>嗯，这是个耗钱的工作。<del>所以等赚到钱了再说，摆烂！</del></p><h2 id="建站机"><a href="#建站机" class="headerlink" title="建站机"></a>建站机</h2><p>最后，是上一篇 MJJ 的延续。</p><p>是的。MJJ 上瘾了。我决定抛弃之前那台香港的 2c4g 杂牌 VPS（天天断网谁受的住啊），然后买一台正经的稳定建站机。</p><p>于是找到了 netcup 这家便宜大碗的商家。经历了<a href="https://samhou.de/notes/aj8dlxjie5wy00u9" rel="external nofollow noreferrer">一点波折</a>后，论坛上收了一个机器，4c8g 512G 高性能德国机器。</p><p>然后，把服务迁移到了那里，现在整个博客的评论系统和 umami 数据分析就在这台机器上，大家可以体验一下套了 cf 之后的速度，应该还不错？</p><p>这台机器我会长期续费了，MJJ 建站毕业机器，除非炸鸡，否则真的不买了。</p><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>这就是第二期日志的全部内容了，因为是公开的杂谈栏目所以话题受限，只有这么多可以写的东西，请期待下一期~</p>]]>
    </content>
    <id>https://blog.samhou.moe/aqua-surf-2/</id>
    <link href="https://blog.samhou.moe/aqua-surf-2/"/>
    <published>2026-03-05T11:03:02.000Z</published>
    <summary>《水星冲浪日志》的第二期，记录了博主初探 PT 下载站的经历，博客换域名的折腾，看板娘形象的确立和画师约稿的成果，以及新入手的高性能建站机器</summary>
    <title>水星冲浪日志 2 —— PT、换域名、约稿和建站机</title>
    <updated>2026-03-05T11:03:02.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Sam Hou</name>
    </author>
    <category term="C++教程" scheme="https://blog.samhou.moe/categories/cpp-tutorial/"/>
    <category term="cpp" scheme="https://blog.samhou.moe/tags/cpp/"/>
    <category term="指针" scheme="https://blog.samhou.moe/tags/%E6%8C%87%E9%92%88/"/>
    <category term="函数" scheme="https://blog.samhou.moe/tags/%E5%87%BD%E6%95%B0/"/>
    <category term="decltype" scheme="https://blog.samhou.moe/tags/decltype/"/>
    <category term="类型别名" scheme="https://blog.samhou.moe/tags/%E7%B1%BB%E5%9E%8B%E5%88%AB%E5%90%8D/"/>
    <content>
      <![CDATA[<p><a href="https://blog.samhou.moe/cpp-overload-match/">上一节</a>我们讲了函数，这次来聊聊<em>函数指针</em>。</p><p>顾名思义，函数指针指的就是，<strong>指向函数的指针</strong>。也就是，指针解引用之后是一个可调用的函数。</p><p>先来看一段示例代码：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-function"><span class="hljs-type">int</span> <span class="hljs-title">addInt</span><span class="hljs-params">(<span class="hljs-type">int</span> a, <span class="hljs-type">int</span> b)</span> </span>&#123;<br>    <span class="hljs-keyword">return</span> a + b;<br>&#125;<br><span class="hljs-function"><span class="hljs-type">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span> </span>&#123;<br>    <span class="hljs-built_in">int</span> (*pAddInt)(<span class="hljs-type">int</span>, <span class="hljs-type">int</span>);<br>    pAddInt = addInt;<br>    cout &lt;&lt; <span class="hljs-built_in">pAddInt</span>(<span class="hljs-number">3</span>, <span class="hljs-number">4</span>) &lt;&lt; endl; <span class="hljs-comment">// 7</span><br>&#125;<br></code></pre></td></tr></table></figure><p>现在我们来仔细看看这段代码做了些什么。</p><h2 id="声明函数指针"><a href="#声明函数指针" class="headerlink" title="声明函数指针"></a>声明函数指针</h2><p>首先我们声明并定义了一个函数 <code>addInt</code>，它接受两个 <code>int</code> 类型的参数，然后返回它们的和。</p><p>然后我们来看今天的重点，main 函数。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-built_in">int</span> (*pAddInt)(<span class="hljs-type">int</span>, <span class="hljs-type">int</span>);<br></code></pre></td></tr></table></figure><p>这就是我们所说的，<strong>函数指针的声明</strong>。</p><p>我们来看看为什么它是函数指针：</p><ul><li><code>pAddInt</code> 是这个变量的名称，我们从这里往外读</li><li>首先是括号，里面有一个 <code>*</code>，说明它是个指针</li><li>右侧的括号中有两个 <code>int</code> 类型，说明这是个参数列表</li><li>左侧的 int 表示一个返回值类型</li><li>因此，它是一个函数指针，可以指向一个函数，这个函数<strong>需要</strong>接受 2 个 int 类型的参数，并返回一个 int</li></ul><p>正如我们在介绍指针时所提到的，指针所指向的内容是有类型限定的，由指针的类型决定。函数指针也是如此，你看，上面的代码已经明确了允许指向的函数的要求了。</p><p>其实，函数指针就是函数加个星号而已，声明和正常的指针没什么差异：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-function"><span class="hljs-type">int</span> <span class="hljs-title">addInt</span><span class="hljs-params">(<span class="hljs-type">int</span>, <span class="hljs-type">int</span>)</span></span>;<br><span class="hljs-built_in">int</span> (*pAddInt)(<span class="hljs-type">int</span>, <span class="hljs-type">int</span>);<br></code></pre></td></tr></table></figure><p>再提醒你一下，在 C++ 中，括号的优先级是比 <code>*</code> 更高的，因此，声明函数指针的时候，必须给星号加上括号。比如，下面的声明是错误的，会声明一个<strong>返回值</strong>为<strong>指向 <code>int</code> 类型的指针</strong>的<strong>函数</strong>，而非<em>函数指针</em>：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-function"><span class="hljs-type">int</span> *<span class="hljs-title">pAddInt</span><span class="hljs-params">(<span class="hljs-type">int</span>, <span class="hljs-type">int</span>)</span></span>;<br></code></pre></td></tr></table></figure><p>是不是看到了指向数组指针的影子？是的，由于括号问题，它们呈现出相似的视觉效果。</p><p>好了，现在我们声明了一个函数指针，但问题是，这个指针现在没有指向任何东西，行为是未定义的。</p><h2 id="赋值函数指针"><a href="#赋值函数指针" class="headerlink" title="赋值函数指针"></a>赋值函数指针</h2><p>我们来看下一行：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs cpp">pAddInt = addInt;<br></code></pre></td></tr></table></figure><p>这一行代码将函数指针 <code>pAddInt</code> 指向了一个实际的函数 <code>addInt</code>。</p><p>但是你或许注意到了一个问题——指针指向的是一个地址，但是为什么这里没有取地址呢？</p><p>首先我们明确一点，函数确实有一个地址，指针也正是指向了这个内存地址。<strong>指针本身</strong>存储了地址，是一个对象，但是<strong>函数</strong>并不是对象（也不能赋值给变量）。</p><p>然后我们再来看为什么没有取地址符号 <code>&amp;</code> 这个问题。</p><p>其实你自己先试一下就会发现，加上这个符号也能通过编译，并正常运行：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs cpp">pAddInt = &amp;addInt;<br></code></pre></td></tr></table></figure><p>这种灵活来自于 C++ 的<strong>退化</strong>性质，当一个表达式中存在函数名字的时候，这个函数会<strong>自动转换为函数指针</strong>。</p><p>是不是很耳熟？没错，这和<strong>数组</strong>是一个道理！还记得吗，我们在<a href="https://blog.samhou.moe/cpp-array-with-pointer/">数组与指针</a>这一节中，曾经提到过，数组在表达式中也会自动转换为指针，指向的是第一个元素。</p><p>函数也是类似，函数名字在表达式中自动转换为指向该函数的函数指针。因此，取地址符号是<strong>可以省略的</strong>。</p><h2 id="调用函数指针"><a href="#调用函数指针" class="headerlink" title="调用函数指针"></a>调用函数指针</h2><p>既然我们已经有了一个函数指针，是时候来看看怎么用了。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs cpp">cout &lt;&lt; <span class="hljs-built_in">pAddInt</span>(<span class="hljs-number">3</span>, <span class="hljs-number">4</span>) &lt;&lt; endl;<br></code></pre></td></tr></table></figure><p>看到了吗？我们正在调用函数指针，正如调用普通的函数一样。</p><p>我知道你要说什么。你肯定想这么干：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs cpp">cout &lt;&lt; (*pAddInt)(<span class="hljs-number">3</span>, <span class="hljs-number">4</span>) &lt;&lt; endl;<br></code></pre></td></tr></table></figure><p>你自己试试就会发现无论是否解引用，表现都是一致的。</p><p>这是因为，编译器为你承担起了这一切——和自动退化相<strong>对称</strong>，解引用也并不是必须的，我愿称之为一组<strong>对称法则</strong>（提示，这不是官方说法，是我个人的总结）。</p><p>实际上，函数指针<strong>可以作为返回值和参数</strong>，就如通常的指针一样。但是，在继续讨论函数指针前，我们先来讲解几个好用的东西，然后再继续深入。</p><h2 id="decltype"><a href="#decltype" class="headerlink" title="decltype"></a>decltype</h2><p>当我们涉及指针的时候，是不是发现声明语句越来越复杂了呢？一个变量的类型可能变得相当复杂：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-function"><span class="hljs-type">int</span>* <span class="hljs-title">addInt</span><span class="hljs-params">(<span class="hljs-type">int</span> *a, <span class="hljs-type">int</span> *b)</span> </span>&#123;<br>    *a = *a + *b;<br>    <span class="hljs-keyword">return</span> a;<br>&#125;<br></code></pre></td></tr></table></figure><p>要创建指向上面函数的指针，我们必须使用这种类型声明语句：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-type">int</span>* (*pAddInt)(<span class="hljs-type">int</span>*, <span class="hljs-type">int</span>*);<br></code></pre></td></tr></table></figure><p>有没有什么简单的方法呢？当然，让我们隆重介绍 <code>decltype</code>！</p><p>看看这个：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-keyword">decltype</span>(addInt) *pAddInt;<br></code></pre></td></tr></table></figure><p><code>decltype</code> 可以把一个<strong>名字</strong>对应的类型，直接<strong>偷过来</strong>！我们这里偷来了 addInt 这个函数的类型（包括参数列表类型和返回值）。通常情况下，这样会声明一个新的函数，但由于我们加上了一个 <code>*</code>，所以我们现在声明的是函数指针。</p><p>一个非常重要的事情是，decltype <strong>不发生退化</strong>，所以记得带上 <code>*</code>。decltype 只是负责推算里面表达式的类型而已，不会帮你转换。</p><p>因此，使用 decltype 可以极大简化代码编写流程，让我们免去书写指针的类型的麻烦。</p><p>注意这个当然不局限于函数指针，而是对于任何指针都是有效的！</p><p>比如，指向数组的指针：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-type">int</span> a[<span class="hljs-number">10</span>];<br><span class="hljs-keyword">decltype</span>(a) *p;<br><span class="hljs-built_in">int</span> (*p2)[<span class="hljs-number">10</span>];<br></code></pre></td></tr></table></figure><p>第二、三行声明的指针，类型都是一样的，都是指向含有 10 个 int 的数组的指针哦。</p><h2 id="类型别名"><a href="#类型别名" class="headerlink" title="类型别名"></a>类型别名</h2><p>明白了 decltype 能够偷来类型，我们再来讲一个能够把类型起个别名的东西，<em>类型别名</em>。</p><p><strong>类型别名</strong>，从名字上来看，就是用另外一个名字，替代原来的类型名字。</p><h3 id="typedef"><a href="#typedef" class="headerlink" title="typedef"></a>typedef</h3><p>传统的类型别名使用方法是 <code>typedef</code>，比如：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-keyword">typedef</span> <span class="hljs-type">double</span> d;<br>d num = <span class="hljs-number">3.14</span>;<br>cout &lt;&lt; num &lt;&lt; endl;<br></code></pre></td></tr></table></figure><p>我们给 double 起了个别名叫做 d。</p><p>但这个太简单了，当类型变得非常复杂的时候，别名才会发挥出作用：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-keyword">typedef</span> <span class="hljs-type">double</span>** d; <span class="hljs-comment">// 指向指针的指针类型</span><br><span class="hljs-keyword">typedef</span> <span class="hljs-type">int</span> arrType[<span class="hljs-number">10</span>]; <span class="hljs-comment">// 包含10个int的数组类型</span><br><span class="hljs-function"><span class="hljs-keyword">typedef</span> <span class="hljs-title">int</span> <span class="hljs-params">(*arrType)</span>[10]</span>; <span class="hljs-comment">// 一个指针类型，指向的是包含10个int的数组类型</span><br><span class="hljs-keyword">typedef</span> <span class="hljs-type">int</span> *arrType[<span class="hljs-number">10</span>]; <span class="hljs-comment">// 包含10个指向int的指针的数组类型</span><br></code></pre></td></tr></table></figure><p>你现在可能非常疑惑，为什么第一条和后面三条的语法<strong>看起来完全不一样</strong>（你看看，第一句的语法好像是 <code>typedef 类型 新的名字</code>，但后面的语法完全不是这样）？换句话说，typedef 到底是如何工作的？</p><p>别被迷惑了，让我们先把 typedef 本身移除掉——</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-type">double</span>** d; <span class="hljs-comment">// 指向指针的指针类型</span><br><span class="hljs-type">int</span> arrType[<span class="hljs-number">10</span>]; <span class="hljs-comment">// 包含10个int的数组类型</span><br><span class="hljs-built_in">int</span> (*arrType)[<span class="hljs-number">10</span>]; <span class="hljs-comment">// 一个指针类型，指向的是包含10个int的数组类型</span><br><span class="hljs-type">int</span> *arrType[<span class="hljs-number">10</span>]; <span class="hljs-comment">// 包含10个指向int的指针的数组类型</span><br></code></pre></td></tr></table></figure><p>现在你可以看出些端倪了：给类型取别名，和<strong>创建变量</strong>的本质没什么区别。</p><p>奶奶都知道可以这么创建变量：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-type">double</span>** dp;<br><span class="hljs-type">int</span> arr[<span class="hljs-number">10</span>];<br><span class="hljs-built_in">int</span> (*arr)[<span class="hljs-number">10</span>];<br><span class="hljs-type">int</span> *arr[<span class="hljs-number">10</span>];<br></code></pre></td></tr></table></figure><p>嗯？是不是发现了什么呢？语法<strong>完全一致</strong>。</p><p>也就是说，typedef 和创建变量的区别只有一个，就是前者<strong>创建的名字是一个可以直接使用的类型</strong>。</p><p>比如：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs cpp">d myd;<br>arrType myArr; <span class="hljs-comment">// 直接当作类型使用！</span><br></code></pre></td></tr></table></figure><p>好了，既然你已经知道如何创建类型别名，那么我们回到函数指针。同理，你可以这么创建函数指针类型：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-function"><span class="hljs-keyword">typedef</span> <span class="hljs-title">int</span> <span class="hljs-params">(*funcPType)</span><span class="hljs-params">(<span class="hljs-type">int</span>*, <span class="hljs-type">int</span>*)</span></span>;<br>funcPType p1; <span class="hljs-comment">// 这是一个函数指针</span><br></code></pre></td></tr></table></figure><p>这样可以节省大量时间。</p><h3 id="using"><a href="#using" class="headerlink" title="using"></a>using</h3><p>在新版的 C++ 中，你可以用另一种方式创建类型别名：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-keyword">using</span> d = <span class="hljs-type">double</span>;<br>d d1 = <span class="hljs-number">3.14</span>;<br>cout &lt;&lt; d1 &lt;&lt; endl; <span class="hljs-comment">// 3.14</span><br></code></pre></td></tr></table></figure><p>这个 using 会把等号后面的类型，起别名，别名名字为等号前的内容。我们可以把上面的 typedef 全部转换为 using：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-keyword">using</span> d = <span class="hljs-type">double</span>** ; <span class="hljs-comment">// 指向指针的指针类型</span><br><span class="hljs-keyword">using</span> arrType = <span class="hljs-type">int</span>[<span class="hljs-number">10</span>]; <span class="hljs-comment">// 包含10个int的数组类型</span><br><span class="hljs-keyword">using</span> arrType = <span class="hljs-built_in">int</span> (*)[<span class="hljs-number">10</span>]; <span class="hljs-comment">// 一个指针类型，指向的是包含10个int的数组类型</span><br><span class="hljs-keyword">using</span> arrType = <span class="hljs-type">int</span>*[<span class="hljs-number">10</span>]; <span class="hljs-comment">// 包含10个指向int的指针的数组类型</span><br></code></pre></td></tr></table></figure><p>很简单吧？只是把名字前置了，相比 typedef，这种方式看起来更加方便。</p><p>当然，也别忘了我们的主角函数指针：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-keyword">using</span> funcPType = <span class="hljs-built_in">int</span> (*)(<span class="hljs-type">int</span>*, <span class="hljs-type">int</span>*);<br></code></pre></td></tr></table></figure><p>更强大的是，你可以把类型别名和 decltype 组合使用，避免写出复杂的类型：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-keyword">using</span> funcType = <span class="hljs-keyword">decltype</span>(func); <span class="hljs-comment">// 现在，你给函数起了别名</span><br>funcType funcP = *funcType; <span class="hljs-comment">// 根据函数别名，创建对应的函数指针</span><br></code></pre></td></tr></table></figure><h2 id="传递函数指针"><a href="#传递函数指针" class="headerlink" title="传递函数指针"></a>传递函数指针</h2><p>既然函数指针本身，是个指针，那么它就变成了一个对象，自然可以传递，就如其它指针一样。</p><h3 id="作为参数传递"><a href="#作为参数传递" class="headerlink" title="作为参数传递"></a>作为参数传递</h3><p>那这有什么用呢？其实，这样可以让同一个函数根据传入的函数指针，执行不同的操作。</p><p>比如，下面的代码，将运算的函数指针传入，然后调用函数执行自定义操作：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-function"><span class="hljs-type">int</span> <span class="hljs-title">sum</span><span class="hljs-params">(<span class="hljs-type">int</span> a, <span class="hljs-type">int</span> b)</span> </span>&#123;<br>    <span class="hljs-keyword">return</span> a + b;<br>&#125;<br><span class="hljs-function"><span class="hljs-type">int</span> <span class="hljs-title">diff</span><span class="hljs-params">(<span class="hljs-type">int</span> a, <span class="hljs-type">int</span> b)</span> </span>&#123;<br>    <span class="hljs-keyword">return</span> <span class="hljs-built_in">abs</span>(a - b);<br>&#125;<br><span class="hljs-keyword">using</span> funcPType = <span class="hljs-keyword">decltype</span>(sum)*;<br><span class="hljs-function"><span class="hljs-type">int</span> <span class="hljs-title">mathOperation</span><span class="hljs-params">(<span class="hljs-type">int</span> a, <span class="hljs-type">int</span> b, funcPType funcP)</span> </span>&#123;<br>    <span class="hljs-keyword">return</span> <span class="hljs-built_in">funcP</span>(a, b);<br>&#125;<br><span class="hljs-function"><span class="hljs-type">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span> </span>&#123;<br>    <span class="hljs-type">int</span> a = <span class="hljs-number">3</span>, b = <span class="hljs-number">4</span>;<br>    cout &lt;&lt; <span class="hljs-built_in">mathOperation</span>(a, b, sum) &lt;&lt; endl; <span class="hljs-comment">// 7</span><br>    cout &lt;&lt; <span class="hljs-built_in">mathOperation</span>(a, b, diff) &lt;&lt; endl; <span class="hljs-comment">// 1</span><br>&#125;<br></code></pre></td></tr></table></figure><p>花点时间好好理解一下上面的代码。</p><p>首先，写了两个执行算术操作的函数。</p><p>然后，我们创建了一个叫做 funcPType 的类型别名，它代表的类型是一个函数指针，指向接受 2 个 int 参数的返回 int 的函数。</p><p>之后，写了一个算术操作的函数，接受函数指针类型，在内部调用这个函数指针，执行相对应的操作。</p><p>最后写了 main 函数，两次传入不同的函数名字。函数名自动退化为函数指针，作为参数传入。你可以看到，我们的 a b 都没有变化，但是结果却不同。这正是因为传入了不同的函数指针导致的。</p><p>也就是说，<strong>函数指针可以把不同操作打包，交给其它部分操作</strong>。也就是，<strong>把行为作为数据传递</strong>。函数指针只记下了传入什么、返回什么，不关心实际执行了什么操作——也正因为如此，我们可以在需要函数指针的地方传入不同的函数，这极大地提升了我们程序的灵活性。</p><p>另外我要提醒你一点，<strong>不要给函数名字后面加上括号</strong>。这样会变成直接调用函数，传递的就是返回值了，会发生类型不匹配直接报错。我们要传递的，是会自动退化成函数指针的<strong>函数名字</strong>。</p><h3 id="作为返回值"><a href="#作为返回值" class="headerlink" title="作为返回值"></a>作为返回值</h3><p>不止参数。函数指针也可以作为<strong>返回值</strong>。</p><p>来看看下面的示例代码（略去和上述相同的 sum 和 diff 函数）：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-keyword">using</span> funcPType = <span class="hljs-keyword">decltype</span>(sum)*;<br><span class="hljs-function">funcPType <span class="hljs-title">chooseOperation</span><span class="hljs-params">(<span class="hljs-type">char</span> c)</span> </span>&#123;<br>    <span class="hljs-keyword">if</span>(c == <span class="hljs-string">&#x27;+&#x27;</span>) <span class="hljs-keyword">return</span> sum;<br>    <span class="hljs-keyword">if</span>(c == <span class="hljs-string">&#x27;-&#x27;</span>) <span class="hljs-keyword">return</span> diff;<br>    <span class="hljs-keyword">return</span> <span class="hljs-literal">nullptr</span>;<br>&#125;<br><span class="hljs-function"><span class="hljs-type">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span> </span>&#123;<br>    <span class="hljs-type">char</span> c1 = <span class="hljs-string">&#x27;+&#x27;</span>;<br>    <span class="hljs-type">char</span> c2 = <span class="hljs-string">&#x27;-&#x27;</span>;<br>    funcPType choice = <span class="hljs-built_in">chooseOperation</span>(c1);<br>    cout &lt;&lt; <span class="hljs-built_in">choice</span>(<span class="hljs-number">1</span>, <span class="hljs-number">3</span>) &lt;&lt; endl; <span class="hljs-comment">// 4</span><br>    choice = <span class="hljs-built_in">chooseOperation</span>(c2);<br>    cout &lt;&lt; <span class="hljs-built_in">choice</span>(<span class="hljs-number">1</span>, <span class="hljs-number">3</span>) &lt;&lt; endl; <span class="hljs-comment">// 2</span><br>&#125;<br></code></pre></td></tr></table></figure><p>仔细看看，我们的函数根据传入的参数不同，返回了不同的函数指针。返回的指针可以作为函数调用——在两条输出语句那里，我们用的是同一个函数指针名称，但是它们指向的是<strong>不同的函数</strong>，也就导致了不同的执行结果。</p><p>这是一个非常现实的例子，同样也是把<strong>行为作为数据传递</strong>的体现。掌握了这种写法，你就可以大大提升你的程序的灵活度。</p><p>此外，我们也可以不用类型别名，但是这会出现一些非常复杂的东西，我们在下面的尾置返回类型中，进行进一步的探讨。</p><h2 id="尾置返回类型"><a href="#尾置返回类型" class="headerlink" title="尾置返回类型"></a>尾置返回类型</h2><p>关于函数指针我们说的够多了，现在我们再来补充一点 C++ 新版本的知识，<strong>尾置返回类型</strong>。</p><p>顾名思义，尾置返回类型允许我们把函数返回值的类型放在末尾。来看看这个：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-function"><span class="hljs-keyword">auto</span> <span class="hljs-title">chooseOperation</span><span class="hljs-params">(<span class="hljs-type">char</span> c)</span> -&gt; funcPType </span>&#123;<br>    <span class="hljs-keyword">if</span>(c == <span class="hljs-string">&#x27;+&#x27;</span>) <span class="hljs-keyword">return</span> sum;<br>    <span class="hljs-keyword">if</span>(c == <span class="hljs-string">&#x27;-&#x27;</span>) <span class="hljs-keyword">return</span> diff;<br>    <span class="hljs-keyword">return</span> <span class="hljs-literal">nullptr</span>;<br>&#125;<br></code></pre></td></tr></table></figure><p>这是上面的代码的另一种写法。原本用于声明函数返回类型的首位被 auto 代替了，真正的返回类型写在最后。</p><p>你可能在想，这不是多此一举吗？其实，只有当你不想用类型别名，但是却想返回函数指针（或任何复杂的类型，比如指向数组的指针）的时候，你会发现这大大被简化了：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-built_in">int</span> (*<span class="hljs-built_in">chooseOperation</span>(<span class="hljs-type">char</span> c))(<span class="hljs-type">int</span>, <span class="hljs-type">int</span>) <span class="hljs-comment">// 一般情况</span><br><span class="hljs-function"><span class="hljs-keyword">auto</span> <span class="hljs-title">chooseOperation</span><span class="hljs-params">(<span class="hljs-type">char</span> c)</span> -&gt; <span class="hljs-title">int</span><span class="hljs-params">(*)</span><span class="hljs-params">(<span class="hljs-type">int</span>, <span class="hljs-type">int</span>)</span> <span class="hljs-comment">// 尾置返回类型，这下看懂了</span></span><br><span class="hljs-function"></span>&#123;<br>    <span class="hljs-keyword">if</span>(c == <span class="hljs-string">&#x27;+&#x27;</span>) <span class="hljs-keyword">return</span> sum;<br>    <span class="hljs-keyword">if</span>(c == <span class="hljs-string">&#x27;-&#x27;</span>) <span class="hljs-keyword">return</span> diff;<br>    <span class="hljs-keyword">return</span> <span class="hljs-literal">nullptr</span>;<br>&#125;<br></code></pre></td></tr></table></figure><p>也补充一下，如果不用尾置返回类型，第一行</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-built_in">int</span> (*<span class="hljs-built_in">chooseOperation</span>(<span class="hljs-type">char</span> c))(<span class="hljs-type">int</span>, <span class="hljs-type">int</span>)<br></code></pre></td></tr></table></figure><p>的含义是：</p><ul><li>从内向外阅读。首先 <code>chooseOperation(char c)</code> 说明此处是个函数。</li><li>其次 <code>(*...)</code> 表示它的返回值是个指针。</li><li>再次，<code>int(...)(int,int)</code> 表示这个指针指向的是个函数，函数接受 2 个 int，返回一个 int。</li></ul><p>如果你有些混乱，不妨翻到文章开头，看看函数指针是如何声明的，对比看看：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-built_in">int</span> (*pAddInt)(<span class="hljs-type">int</span>, <span class="hljs-type">int</span>);<br><span class="hljs-built_in">int</span> (*<span class="hljs-built_in">chooseOperation</span>(<span class="hljs-type">char</span> c))(<span class="hljs-type">int</span>, <span class="hljs-type">int</span>)<br></code></pre></td></tr></table></figure><p>现在是不是发现了点规律？只是把变量名字换成函数名字+参数列表而已。</p><p>但是，我完全不建议你这么写。请一定要使用尾置返回类型、类型别名和 decltype，让你的代码更加可读。</p><p>关于函数指针和简化代码的方法，我们已经涉及了相当深入的内容了。下一节，我们将离开函数这一篇章，进入新的一部分——<strong>类</strong>。</p>]]>
    </content>
    <id>https://blog.samhou.moe/cpp-function-pointer-type/</id>
    <link href="https://blog.samhou.moe/cpp-function-pointer-type/"/>
    <published>2026-03-04T06:03:01.000Z</published>
    <summary>本文深入讲解了C++中的函数指针、decltype、类型别名和尾置返回类型。通过通俗易懂的实例，详细拆解了函数指针的声明、赋值、调用及其在参数传递、函数返回等场景的应用，并探讨了如何利用现代C++特性简化复杂类型声明，使代码更清晰灵活。</summary>
    <title>奶奶都能看懂的 C++ —— 函数指针、decltype、类型别名和尾置返回</title>
    <updated>2026-03-04T06:03:01.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Sam Hou</name>
    </author>
    <category term="csharp" scheme="https://blog.samhou.moe/categories/csharp/"/>
    <category term="csharp" scheme="https://blog.samhou.moe/tags/csharp/"/>
    <category term="linq" scheme="https://blog.samhou.moe/tags/linq/"/>
    <category term="lambda" scheme="https://blog.samhou.moe/tags/lambda/"/>
    <category term="ienumerable" scheme="https://blog.samhou.moe/tags/ienumerable/"/>
    <content>
      <![CDATA[<p><a href="https://blog.samhou.moe/csharp-linq-guide/">上一篇</a>，我们讲了 LINQ 的入门知识，了解了方法和声明式查询这两种形式，但是对于将声明式查询的一部分，转换为方法，还没有深入了解。</p><p>这一篇，我们将从 Lambda 表达式开始，一步一步带你走进 LINQ 方法的世界，最终自己实现 IEnumerable！</p><p>让我们开始吧。</p><h2 id="Lambda-表达式"><a href="#Lambda-表达式" class="headerlink" title="Lambda 表达式"></a>Lambda 表达式</h2><p>在阅读 C# 代码的时候，你一定时常碰见这个 <code>=&gt;</code>。</p><p>比如：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">override</span> <span class="hljs-built_in">string</span> <span class="hljs-title">ToString</span>()</span> =&gt; <span class="hljs-string">&quot;String&quot;</span>;<br></code></pre></td></tr></table></figure><p>上面的例子非常直观，你一定能猜到它是怎么工作的：让这个方法的返回值为 <code>&quot;String&quot;</code>。</p><p><code>=&gt;</code> 这个小符号，定义的就是一个 <em>Lambda 表达式</em>。它声明了一个<em>匿名函数</em>（没有名字的方法），我们可以把它的语法总结为：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs csharp">(参数类型 参数<span class="hljs-number">1</span>, 参数类型 参数<span class="hljs-number">2</span> ...) =&gt; 表达式;<br></code></pre></td></tr></table></figure><p>括号内部的，是匿名函数的参数列表；表达式表示的值，就是这个匿名函数的<em>返回值</em>。</p><p>也就是说，我们可以把很多单个 return 的方法，借助 lambda 表达式，直接简化为一行！</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">static</span> <span class="hljs-keyword">class</span> <span class="hljs-title">AwesomeClass</span><br>&#123;<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-built_in">string</span> <span class="hljs-title">Shout</span>(<span class="hljs-params"><span class="hljs-built_in">string</span> s</span>)</span> =&gt; <span class="hljs-string">&quot;OHHHHHHHHHHH THIS IS SIMPLE&quot;</span>;<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-built_in">bool</span> <span class="hljs-title">IsOverHundred</span>(<span class="hljs-params"><span class="hljs-built_in">int</span> num</span>)</span> =&gt; num &gt; <span class="hljs-number">100</span>;<br>&#125;<br></code></pre></td></tr></table></figure><p>第一个不再赘述，第二个的含义是，接受 num，然后计算 &#x3D;&gt; 后面的表达式，返回表达式的值（即，检测 num 是否大于100）</p><p><em>等等</em>。你一定在思考一个问题：刚才提到说，lambda 表达式创建的是<strong>匿名方法</strong>，但是上面的示例代码里面，不还是有名字吗？？这个<strong>匿名</strong>，到底是什么意思呢？</p><p>欸，实际上，lambda 表达式定义的是<strong>一段函数逻辑</strong>，本身确实是<strong>没有名字</strong>的。</p><p>但是，它可以用在<strong>任何需要一个函数体的地方</strong>，包括上面的 <code>ToString</code> 和 <code>Shout</code>。正是因为你把这个匿名方法用到了一个有名字的类成员身上，所以才<em>看起来</em>有名字。</p><p>也就是说，上面代码中，涉及 lambda 和匿名函数的，只有：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs csharp">() =&gt; <span class="hljs-string">&quot;String&quot;</span>;<br>(<span class="hljs-built_in">string</span> s) =&gt; <span class="hljs-string">&quot;OHHHHHHHHHHH THIS IS SIMPLE&quot;</span>;<br><span class="hljs-comment">// 也就是……</span><br>(输入) =&gt; 输出;<br></code></pre></td></tr></table></figure><p>并不包含括号前面的那个名字。</p><p>那么问题来了，这和我们的 LINQ 又有什么关系呢？</p><h2 id="LINQ-中的-Lambda"><a href="#LINQ-中的-Lambda" class="headerlink" title="LINQ 中的 Lambda"></a>LINQ 中的 Lambda</h2><h3 id="入门：过滤-Where"><a href="#入门：过滤-Where" class="headerlink" title="入门：过滤 Where"></a>入门：过滤 Where</h3><p>让我们先回忆上一篇的一段声明式查询语句：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">var</span> result = <span class="hljs-keyword">from</span> num <span class="hljs-keyword">in</span> numbers<br>             <span class="hljs-keyword">where</span> num &gt; <span class="hljs-number">500</span><br>             <span class="hljs-keyword">orderby</span> num<br>             <span class="hljs-keyword">select</span> num + <span class="hljs-number">1</span>;<br></code></pre></td></tr></table></figure><p>我们已经提到，声明式查询可以改写为方法串链。既然是直接在原来的集合上面调用，那么方法串链的形式不需要 from；那么，现在来试试输入 <code>Where()</code> 吧：</p><p><img src="https://img.samhou.top/1770188472527.webp" alt="智能提示"></p><p>来了点奇怪的东西！IDE 告诉我们，Where 这个方法，需要一个 <code>Func&lt;int,bool&gt;</code> 作为参数……？</p><p>点进去看一下（反编译）：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-built_in">delegate</span> TResult <span class="hljs-title">Func</span>&lt;<span class="hljs-keyword">in</span> <span class="hljs-title">T</span>, <span class="hljs-keyword">out</span> <span class="hljs-title">TResult</span>&gt;(<span class="hljs-params">T arg</span>)</span>;<br></code></pre></td></tr></table></figure><blockquote><p><em>delegate</em>！这是一个<em>委托</em>。<br>关于委托，这并不在本文的讨论范围内。先用 LINQ 和 Lambda 的思维看看吧。</p></blockquote><p>你可以简单理解为，这里<strong>需要一个方法</strong>，以 T 作为参数，TResult 作为结果。就这么个简单的 LINQ 查询，你肯定不想独立写一个方法出来——因此，是时候让匿名函数出手了：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs csharp">numbers.Where(num =&gt; num &gt; <span class="hljs-number">500</span>)<br></code></pre></td></tr></table></figure><p><code>Func&lt;int,bool&gt;</code> 表示参数类型 int，返回值类型 bool。上面的查询中需要<strong>一种逻辑</strong>：</p><p>“如果通过参数传入的 int <strong>满足某些条件</strong>，那么返回 true 保留；否则，返回 false 丢掉。”</p><p>因此我们写了下面这样的 lambda 表达式，让这个匿名函数对于参数大于 500 的情况返回 True，否则返回 False。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs csharp">num =&gt; num &gt; <span class="hljs-number">500</span><br></code></pre></td></tr></table></figure><p>我知道你一定又有问题了。</p><p>明明我们的 lambda 表达式应该是这样的：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs csharp">(参数类型 参数) =&gt; 表达式;<br></code></pre></td></tr></table></figure><p>为什么括号没了，甚至参数类型也没了！？？</p><p>这是因为——如果这个 lambda 作为一个参数 (<code>Func&lt;T,T&gt;</code>) 传入另一个方法，那么这个匿名函数的<em>参数</em>和<em>返回值</em>的<strong>类型</strong>，是<strong>确定已知</strong>的，因此，<strong>没有必要</strong>去写参数的类型。</p><p>而又由于只有一个参数名称，所以括号是不必要的，可以删除，最终得到这样的简化式子：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs csharp">参数 =&gt; 表达式<br></code></pre></td></tr></table></figure><p>要提醒的是，千万别忘了，lambda 的参数和方法一样，是要<strong>自己命名的</strong>，这里省略的是参数类型，不是参数名字本身！</p><p>因此，<strong>当你看到 <code>Func&lt;T,T&gt;</code> 类型的参数的时候，请明白，这里需要一个处理逻辑，也就是一个 lambda 表达式</strong>。</p><h3 id="排序-Orderby"><a href="#排序-Orderby" class="headerlink" title="排序 Orderby"></a>排序 Orderby</h3><p>声明式查询的第二个子句，是 orderby。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">orderby</span> num<br></code></pre></td></tr></table></figure><p>现在我们在刚才的 Where 后面，串接上 orderby：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs csharp">numbers.Where((<span class="hljs-built_in">int</span> num) =&gt; num &gt; <span class="hljs-number">500</span>)<br>    .OrderBy(num=&gt;num);<br></code></pre></td></tr></table></figure><p>来自己试试吧！把鼠标悬浮在 orderby 上面，你会发现它需要的是 <code>Func&lt;int,TKey&gt;</code> 类型的一个参数。</p><p>TKey 是啥？看看注释：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs text">... in ascending order according to a key.<br></code></pre></td></tr></table></figure><p>哦！我们知道了，我们需要一种逻辑，传入一个列表元素，返回一个可以排序的东西（叫做 key）。然后 orderby 会根据这个 key，排序原始传入的列表元素。</p><p>在我们的情况下，由于数据源是 int 类型列表，所以传入的对象是 int。</p><p>仔细想想之后，会发现——我们<strong>就是要根据这个 int 类型的列表元素本身</strong>，来进行排序！</p><p>所以根本就不需要进行任何处理，直接返回 num 本身不就好了？</p><p>所以……</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs csharp">num =&gt; num<br></code></pre></td></tr></table></figure><p>我知道这个看起来多此一举，但是总不能什么都不填……这个 lambda 的含义是，接收到参数命名为 num，按照原样返回 num！</p><p>实际开发中肯定不是排序数字列表这么简单，再给你举一个不那么“多此一举”的例子（继续拿上一篇中的 User 类）：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-built_in">enum</span> Status<br>&#123;<br>    Offline,<br>    Online<br>&#125;<br><span class="hljs-keyword">class</span> <span class="hljs-title">User</span><br>&#123;<br>    <span class="hljs-keyword">public</span> <span class="hljs-built_in">int</span> Id &#123; <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; &#125;<br>    <span class="hljs-keyword">public</span> Status Status &#123; <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>如果有个 User 列表，就能这么写：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs csharp">users.OrderBy(user=&gt;user.Id)<br></code></pre></td></tr></table></figure><p>这里的含义是，把 users 列表，按照其每个 User 类实例（命名为 user）的 Id 排序。</p><p>我们把其等价声明式查询拿出来——</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">from</span> user <span class="hljs-keyword">in</span> users <span class="hljs-keyword">orderby</span> user.Id <span class="hljs-keyword">select</span> user<br></code></pre></td></tr></table></figure><p>比对一下吧！把每个部分这么一对应，是不是声明式查询和方法查询都变得十分清晰了呢？</p><h3 id="分组-Group"><a href="#分组-Group" class="headerlink" title="分组 Group"></a>分组 Group</h3><p>还记得上一篇文章里面介绍的分组吗？现在也来介绍一下吧！</p><p>我们先拿出</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><code class="hljs csharp">User[] users = <span class="hljs-keyword">new</span> User[]<br>&#123;<br>    <span class="hljs-keyword">new</span> User &#123; Id = <span class="hljs-number">1</span>, Status = Status.Online &#125;,<br>    <span class="hljs-keyword">new</span> User &#123; Id = <span class="hljs-number">2</span>, Status = Status.Online &#125;,<br>    <span class="hljs-keyword">new</span> User &#123; Id = <span class="hljs-number">3</span>, Status = Status.Offline &#125;,<br>    <span class="hljs-keyword">new</span> User &#123; Id = <span class="hljs-number">4</span>, Status = Status.Online &#125;,<br>&#125;;<br><span class="hljs-keyword">var</span> result=<span class="hljs-keyword">from</span> user <span class="hljs-keyword">in</span> users <span class="hljs-keyword">orderby</span> user.Id<br>           <span class="hljs-keyword">group</span> user <span class="hljs-keyword">by</span> user.Status <span class="hljs-keyword">into</span> userGroup<br>           <span class="hljs-keyword">select</span> userGroup;<br><span class="hljs-keyword">foreach</span> (<span class="hljs-keyword">var</span> <span class="hljs-keyword">group</span> <span class="hljs-keyword">in</span> result)<br>&#123;<br>    Console.WriteLine(<span class="hljs-string">&quot;Group &quot;</span> + <span class="hljs-keyword">group</span>.Key);<br>    <span class="hljs-keyword">foreach</span> (<span class="hljs-keyword">var</span> item <span class="hljs-keyword">in</span> <span class="hljs-keyword">group</span>)<br>    &#123;<br>        Console.WriteLine(<span class="hljs-string">&quot;User ID #&quot;</span> + item.Id<br>            + <span class="hljs-string">&quot; Status: &quot;</span> + item.Status);<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>先自己尝试一下吧！先按照上面的讲解写出 OrderBy()，然后输入 GroupBy()，把鼠标放上去查看它需要接受一个怎样的参数。</p><p>答案是：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">var</span> result = users.OrderBy(user =&gt; user.Id)<br>    .GroupBy(user =&gt; user.Status);<br></code></pre></td></tr></table></figure><p>写出来了吗？</p><p>现在来拆解一下：</p><p>GroupBy 接受一个 <code>Func&lt;User,TKey&gt;</code> 参数，含义是输入一个 User，根据 lambda 中匿名函数返回的 TKey 类型的数据来分组。相同的 TKey 类型分到一组。因此，我们这里是根据 user.Status 进行分组，TKey 这个泛型就变成了 Status enum。</p><p>好了，不啰嗦了。Group 和 Orderby 实在非常相似，自己对照一下，相信你一定可以明白。</p><h3 id="合并-Join"><a href="#合并-Join" class="headerlink" title="合并 Join"></a>合并 Join</h3><p>上一篇，我们写了这样一个用户留言板程序，生成了匿名类型：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">class</span> <span class="hljs-title">User</span> <span class="hljs-comment">// 这个类没有改</span><br>&#123;<br>    <span class="hljs-keyword">public</span> <span class="hljs-built_in">int</span> Id &#123; <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; &#125;<br>    <span class="hljs-keyword">public</span> Status Status &#123; <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; &#125;<br>&#125;<br><span class="hljs-keyword">class</span> <span class="hljs-title">Message</span> <span class="hljs-comment">// 表示用户发送的一条信息</span><br>&#123;<br>    <span class="hljs-comment">// 发送者 ID</span><br>    <span class="hljs-keyword">public</span> <span class="hljs-built_in">int</span> SenderId &#123;  <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; &#125;<br>    <span class="hljs-comment">// 发送内容</span><br>    <span class="hljs-keyword">public</span> <span class="hljs-built_in">string</span> Text &#123; <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; &#125; = <span class="hljs-string">&quot;&quot;</span>;<br>&#125;<br></code></pre></td></tr></table></figure><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><code class="hljs csharp">User[] users = <span class="hljs-keyword">new</span> User[] <span class="hljs-comment">// 不变</span><br>&#123;<br>    <span class="hljs-keyword">new</span> User &#123; Id = <span class="hljs-number">1</span>, Status = Status.Online &#125;,<br>    <span class="hljs-keyword">new</span> User &#123; Id = <span class="hljs-number">2</span>, Status = Status.Online &#125;,<br>    <span class="hljs-keyword">new</span> User &#123; Id = <span class="hljs-number">3</span>, Status = Status.Offline &#125;,<br>    <span class="hljs-keyword">new</span> User &#123; Id = <span class="hljs-number">4</span>, Status = Status.Online &#125;,<br>&#125;;<br>Message[] messages = <span class="hljs-keyword">new</span> Message[] <span class="hljs-comment">// 来构造一个示例消息列表</span><br>&#123;<br>    <span class="hljs-keyword">new</span> Message &#123;SenderId= <span class="hljs-number">1</span>,Text=<span class="hljs-string">&quot;I love this.&quot;</span>&#125;,<br>    <span class="hljs-keyword">new</span> Message &#123;SenderId= <span class="hljs-number">2</span>,Text=<span class="hljs-string">&quot;No wayyyyy we can leave message&quot;</span> &#125;,<br>    <span class="hljs-keyword">new</span> Message&#123;SenderId=<span class="hljs-number">3</span>,Text=<span class="hljs-string">&quot;OMG this is crazy&quot;</span>&#125;,<br>    <span class="hljs-keyword">new</span> Message&#123;SenderId=<span class="hljs-number">3</span>,Text=<span class="hljs-string">&quot;Great work!&quot;</span>&#125;,<br>    <span class="hljs-keyword">new</span> Message&#123;SenderId=<span class="hljs-number">4</span>,Text=<span class="hljs-string">&quot;Can I delete my message???&quot;</span>&#125;<br>&#125;;<br><span class="hljs-keyword">var</span> result = <span class="hljs-keyword">from</span> message <span class="hljs-keyword">in</span> messages<br>             <span class="hljs-keyword">join</span> user <span class="hljs-keyword">in</span> users<br>             <span class="hljs-keyword">on</span> message.SenderId <span class="hljs-keyword">equals</span> user.Id<br>             <span class="hljs-keyword">select</span> <span class="hljs-keyword">new</span><br>             &#123;<br>                 SenderId = message.SenderId,<br>                 Text = message.Text,<br>                 UserStatus = user.Status,<br>             &#125;;<br><span class="hljs-keyword">foreach</span> (<span class="hljs-keyword">var</span> item <span class="hljs-keyword">in</span> result)<br>&#123;<br>    Console.WriteLine(<span class="hljs-string">&quot;Message [&quot;</span> + item.Text +<br>        <span class="hljs-string">&quot;] from user #&quot;</span> + item.SenderId +<br>        <span class="hljs-string">&quot; whose status is &quot;</span> + item.UserStatus);<br>&#125;<br></code></pre></td></tr></table></figure><p>现在我们来看这个 Join——来试试输入吧！</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs csharp">messages.Join( <span class="hljs-comment">// 试着打这些</span><br></code></pre></td></tr></table></figure><p>IDE 里面智能提示太长了放不下！我们去 <a href="https://learn.microsoft.com/zh-cn/dotnet/api/system.linq.enumerable.join" rel="external nofollow noreferrer">Microsoft Learn</a> 查一下这个方法（阅读文档是一种非常好的学习方式）。</p><blockquote><p>Correlates the elements of two sequences based on matching keys.</p></blockquote><p>由于这是一个<em>扩展方法</em>（此处不展开），所以带着 this 的参数直接忽略，它表示 .Join 前面的那个对象。</p><p>还剩下 4 个参数：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs csharp">System.Collections.Generic.IEnumerable&lt;TInner&gt; inner,<br>Func&lt;TOuter,TKey&gt; outerKeySelector, <br>Func&lt;TInner,TKey&gt; innerKeySelector, <br>Func&lt;TOuter,TInner,TResult&gt; resultSelector<br></code></pre></td></tr></table></figure><blockquote><p>Inner: The sequence to join to the first sequence.</p></blockquote><p>也就是说，第一个参数是需要拼接到目标对象的序列。我们的情境下，是 <code>users</code> 序列。</p><p>下面的两个参数是：</p><blockquote><p>A function to extract the join key from each element of the first sequence.<br>A function to extract the join key from each element of the second sequence.</p></blockquote><p>哦~理解了，就是给两个序列，分别写两个 lambda 表达式，返回两个属性，会判断它们是否匹配！也就是：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">on</span> message.SenderId <span class="hljs-keyword">equals</span> user.Id<br></code></pre></td></tr></table></figure><p>那么，我们写出这样的 lambda 表达式就行了：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs csharp">message=&gt;message.SenderId<br>user=&gt;user.Id<br></code></pre></td></tr></table></figure><p>最后当然就是 select 啦，我们就在这里创建新的匿名类型：</p><blockquote><p>A function to create a result element from two matching elements.</p></blockquote><p><strong>注意了！由于我们有两个序列，所以需要 2 个参数的匿名方法。两个参数就不可以省略括号了。</strong></p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs csharp">(message, user) =&gt; <span class="hljs-keyword">new</span><br>    &#123;<br>        SenderId = user.Id,<br>        Text = message.Text,<br>        UserStatus = user.Status,<br>    &#125;<br></code></pre></td></tr></table></figure><p>完美！现在让我们展示一下最终的结果。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">var</span> result = messages.Join(users, message =&gt; message.SenderId,<br>    user =&gt; user.Id, (message, user) =&gt; <span class="hljs-keyword">new</span><br>    &#123;<br>        SenderId = user.Id,<br>        Text = message.Text,<br>        UserStatus = user.Status,<br>    &#125;);<br></code></pre></td></tr></table></figure><p>你觉得哪种，声明式，还是直接写方法比较舒服呢？其实这取决于你自己——写出来的代码只要清晰易懂即可。</p><h2 id="实现-IEnumerable"><a href="#实现-IEnumerable" class="headerlink" title="实现 IEnumerable"></a>实现 IEnumerable</h2><h3 id="手写实现接口"><a href="#手写实现接口" class="headerlink" title="手写实现接口"></a>手写实现接口</h3><p>对于 LINQ，现在你已经有相当深入的了解了。但是，你还记得我在上一篇开头的地方，给你留的小剧透吗？</p><blockquote><p>在本文的后半，会介绍怎么自己实现这个接口，不过现在我们先记住这个前提，然后来用一下 LINQ 感受一下吧！</p></blockquote><p>对于数据查询，你已经几乎离不开 LINQ 了不是吗？</p><p>因此，你当然希望你的数据——我是指，你自己创建的类，也应该能够实现 LINQ 查询对吧。</p><p>现在让我们来探索这个接口。</p><p>比如，我们来创建一个自定义的用户列表 UserList 类型！</p><p>键入下面的 class 语句：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">class</span> <span class="hljs-title">UserList</span> : <span class="hljs-title">IEnumerable</span>&lt;<span class="hljs-title">User</span>&gt;<br></code></pre></td></tr></table></figure><p>现在在波浪线的地方按下 alt + enter，选中“实现接口”。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">class</span> <span class="hljs-title">UserList</span> : <span class="hljs-title">IEnumerable</span>&lt;<span class="hljs-title">User</span>&gt;<br>&#123;<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> IEnumerator&lt;User&gt; <span class="hljs-title">GetEnumerator</span>()</span><br>    &#123;<br>        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> NotImplementedException();<br>    &#125;<br>    IEnumerator IEnumerable.GetEnumerator()<br>    &#123;<br>        <span class="hljs-keyword">return</span> GetEnumerator();<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>现在你知道这个接口必须实现这两个方法。那么问题来了，IDE 生成的这个 <code>IEnumerator&lt;User&gt;</code> 接口（更通用地，是 <code>IEnumerator&lt;T&gt;</code>），又是什么？？？</p><p>从名字上理解，这个叫做<strong>枚举器</strong>，也就是把一个集合里面的东西<strong>一个个枚举出来</strong>的方法。</p><blockquote><p>如果你看过《<a href="https://blog.samhou.moe/cpp-vector-iterator-guide/">奶奶都能看懂的 C++ —— vector 与迭代器</a>》这篇文章或者你是 C++ 高手，一个很好的方法就是，把枚举器看成 C++ 迭代器，但是不需要解引用。（没看过也没关系！下面会从零开始详细解释枚举器）</p></blockquote><p>我们不妨来创建一个类，来让 IDE 实现这个接口：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">class</span> <span class="hljs-title">UserListEnumerator</span> : <span class="hljs-title">IEnumerator</span>&lt;<span class="hljs-title">User</span>&gt;<br>&#123;<br>    <span class="hljs-keyword">public</span> User Current =&gt; <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> NotImplementedException();<br>    <span class="hljs-built_in">object</span> IEnumerator.Current =&gt; Current;<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Dispose</span>()</span><br>    &#123;<br>        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> NotImplementedException();<br>    &#125;<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-built_in">bool</span> <span class="hljs-title">MoveNext</span>()</span><br>    &#123;<br>        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> NotImplementedException();<br>    &#125;<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Reset</span>()</span><br>    &#123;<br>        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> NotImplementedException();<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>WOW，出来了一堆方法！</p><p>我们来结合含义讲解一下。</p><ul><li>Current、Move、Reset，看得出来，好像有什么在移动</li><li>没错，这个接口表示的就是一个<strong>枚举器</strong>，会从序列开头移动到末尾</li><li>Current 返回枚举器当前指向的对象</li><li>MoveNext 把枚举器移到下一位，如果移位后 Current 指向的对象有效，那么返回 ture；如果已经越过列表末尾了，那么返回 false</li><li>Reset 重置枚举器</li><li>Dispose 会释放枚举器使用的资源（此处略去，不在讨论范围内）</li></ul><p>现在让我们来实现！</p><p>但是，我们的 UserList 明明自称是一个可枚举的列表，里面却没有数据。由于这个程序只是一个演示用途，因此，我们通过构造函数传入，来初始化这个列表：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">class</span> <span class="hljs-title">UserList</span> : <span class="hljs-title">IEnumerable</span>&lt;<span class="hljs-title">User</span>&gt;<br>&#123;<br>    User[] _users;<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">UserList</span>(<span class="hljs-params">User[] users</span>)</span><br>    &#123;<br>        <span class="hljs-comment">// 传入！</span><br>        _users = users;<br>    &#125;<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> IEnumerator&lt;User&gt; <span class="hljs-title">GetEnumerator</span>()</span><br>    &#123;<br>        <span class="hljs-comment">// 根据现有数据生成枚举器！</span><br>        <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> UserListEnumerator(_users);<br>    &#125;<br>    IEnumerator IEnumerable.GetEnumerator()<br>    &#123;<br>        <span class="hljs-keyword">return</span> GetEnumerator();<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>再来手动实现枚举器的逻辑——注意，<strong>第一次迭代</strong>的时候就会调用 MoveNext，因此下标从 -1 开始：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">class</span> <span class="hljs-title">UserListEnumerator</span> : <span class="hljs-title">IEnumerator</span>&lt;<span class="hljs-title">User</span>&gt;<br>&#123;<br>    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> User[] _users;<br>    <span class="hljs-built_in">int</span> _index = <span class="hljs-number">-1</span>; <span class="hljs-comment">// 从 -1 开始！</span><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">UserListEnumerator</span>(<span class="hljs-params">User[] users</span>)</span><br>    &#123;<br>        _users = users;<br>    &#125;<br>    <span class="hljs-keyword">public</span> User Current =&gt; _users[_index]; <span class="hljs-comment">// 当前的元素</span><br>    <span class="hljs-built_in">object</span> IEnumerator.Current =&gt; Current;<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Dispose</span>()</span><br>    &#123;<br>    <span class="hljs-comment">// 略去。</span><br>    &#125;<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-built_in">bool</span> <span class="hljs-title">MoveNext</span>()</span><br>    &#123;<br>        <span class="hljs-comment">// 移到下一位</span><br>        _index++;<br>        <span class="hljs-comment">// 检查移位之后，当前的 Current 是否有效</span><br>        <span class="hljs-keyword">return</span> _index &lt; _users.Length;<br>    &#125;<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Reset</span>()</span><br>    &#123;<br>        _index = <span class="hljs-number">-1</span>;<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>确实，手动写这么一大堆东西，头都大了，有什么更加简单方便的方法来实现枚举器吗？</p><h3 id="yield-return"><a href="#yield-return" class="headerlink" title="yield return"></a>yield return</h3><p>当然！隆重介绍 <code>yield return</code>！它可以全自动地生成一个枚举器，请看示例代码——</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">class</span> <span class="hljs-title">UserList</span> : <span class="hljs-title">IEnumerable</span>&lt;<span class="hljs-title">User</span>&gt;<br>&#123;<br>    User[] _users;<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">UserList</span>(<span class="hljs-params">User[] users</span>)</span><br>    &#123;<br>        _users = users;<br>    &#125;<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> IEnumerator&lt;User&gt; <span class="hljs-title">GetEnumerator</span>()</span><br>    &#123;<br>        <span class="hljs-keyword">for</span>(<span class="hljs-built_in">int</span> i = <span class="hljs-number">0</span>; i &lt; _users.Length; i++)<br>        &#123;<br>            <span class="hljs-keyword">yield</span> <span class="hljs-keyword">return</span> _users[i];<br>        &#125;<br>    &#125;<br>    IEnumerator IEnumerable.GetEnumerator()<br>    &#123;<br>        <span class="hljs-keyword">return</span> GetEnumerator();<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>你可能已经发现，有一个类全部消失了！这就是 yield return，它可以全自动生成一个 <code>IEnumerator&lt;User&gt;</code>。</p><p>但是<em>等等</em>。</p><blockquote><p>这到底是什么原理？我枚举这个列表的时候，代码究竟在怎么执行？为什么 yield return 可以返回一个枚举器？</p></blockquote><p>其实，yield return 就是两句话：</p><ul><li>枚举时，yield return 返回当前枚举的元素，然后保存这个执行状态</li><li>到下一次迭代时，从上一次 yield return 处，继续执行。</li></ul><p>不妨来做个实验吧！我们把 yield return 上下改成这样：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">for</span>(<span class="hljs-built_in">int</span> i = <span class="hljs-number">0</span>; i &lt; _users.Length; i++)<br>&#123;<br>    Console.WriteLine(<span class="hljs-string">&quot;Start Yield return #&quot;</span>+(i+<span class="hljs-number">1</span>));<br>    <span class="hljs-keyword">yield</span> <span class="hljs-keyword">return</span> _users[i];<br>    Console.WriteLine(<span class="hljs-string">&quot;End Yield return #&quot;</span>+(i+<span class="hljs-number">1</span>));<br>&#125;<br></code></pre></td></tr></table></figure><p>然后写一个 foreach 方法（没错，实现了 IEnumerable 就能用 foreach 了）</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs csharp">UserList userList=<span class="hljs-keyword">new</span>(users);<br><span class="hljs-keyword">foreach</span> (User user <span class="hljs-keyword">in</span> userList)<br>&#123;<br>    Console.WriteLine(<span class="hljs-string">&quot;Getting user #&quot;</span> + user.Id);<br>&#125;<br></code></pre></td></tr></table></figure><p>试试看吧！</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs text">Start Yield return #1<br>Getting user #1<br>End Yield return #1<br>Start Yield return #2<br>Getting user #2<br>End Yield return #2<br>Start Yield return #3<br>Getting user #3<br>End Yield return #3<br>Start Yield return #4<br>Getting user #4<br>End Yield return #4<br></code></pre></td></tr></table></figure><p>看到了吗？枚举的时候发生了这样的事情：</p><ul><li>获取第一个元素，执行到第一次 yield return，中断</li><li>获取第二个元素，从上次中断处继续执行，直到遇到下一次 yield return</li><li>……</li></ul><p>因此，返回的不是一个东西，而是一组：yield return 允许你创建一个<strong>全自动管理的</strong>枚举器，不需要手写，会根据需要执行——每一次 yield return，都会吐出一个元素，都是一次<strong>中断与恢复</strong>的过程。</p><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>对于 LINQ，我们已经进行了非常深入的讲解，还涉及到了 lambda 表达式，以及 yield return 的核心原理。希望你有所收获！</p>]]>
    </content>
    <id>https://blog.samhou.moe/csharp-linq-lambda-enumerable/</id>
    <link href="https://blog.samhou.moe/csharp-linq-lambda-enumerable/"/>
    <published>2026-02-04T11:02:29.000Z</published>
    <summary>本文深入讲解 C# 中 LINQ 与 Lambda 表达式，介绍如何将声明式查询转换为方法链调用，详细拆解 Where、OrderBy、GroupBy 和 Join 等关键操作。最后通过实现 IEnumerable 接口与 yield return 机制，揭示 LINQ 查询的底层迭代原理。</summary>
    <title>奶奶都能看懂的 C# —— LINQ、 Lambda 和 IEnumerable</title>
    <updated>2026-02-04T11:02:29.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Sam Hou</name>
    </author>
    <category term="csharp" scheme="https://blog.samhou.moe/categories/csharp/"/>
    <category term="csharp" scheme="https://blog.samhou.moe/tags/csharp/"/>
    <category term="linq" scheme="https://blog.samhou.moe/tags/linq/"/>
    <content>
      <![CDATA[<p>LINQ，学了会，会了忘，忘了学，学了又会，会了又忘……</p><p>受不了了！今天这篇文章就来讲解一下 C# 中的 LINQ，先入门，然后带你从<strong>代码内涵</strong>上，帮助记忆 LINQ 语法。</p><h2 id="标准-LINQ-的使用前提"><a href="#标准-LINQ-的使用前提" class="headerlink" title="标准 LINQ 的使用前提"></a>标准 LINQ 的使用前提</h2><p>在 C# 的世界里，有许许多多各式各样的对象，也有这些对象的集合，比如我们熟悉的 <code>List&lt;T&gt;</code>。</p><p>LINQ 就是为了对这些集合类型的数据进行查询和处理而生的。它的全名叫做 <code>Language-Integrated Query</code>，语言中集成的查询，很好的名字不是吗？</p><p>在正式开始查询之前，我们先来说说什么情况下能用。</p><p>动动手，打开 VS 创建一个 <code>List</code> 实例，把光标放在这个类型上面，然后按下 F12 导航到定义（反编译源码）。仔细观察这种“集合”类型的对象实现了哪些接口：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">List</span>&lt;<span class="hljs-title">T</span>&gt; : <span class="hljs-title">ICollection</span>&lt;<span class="hljs-title">T</span>&gt;, <span class="hljs-title">IEnumerable</span>&lt;<span class="hljs-title">T</span>&gt;, <span class="hljs-title">IEnumerable</span>, <span class="hljs-title">IList</span>&lt;<span class="hljs-title">T</span>&gt;, <span class="hljs-title">IReadOnlyCollection</span>&lt;<span class="hljs-title">T</span>&gt;, <span class="hljs-title">IReadOnlyList</span>&lt;<span class="hljs-title">T</span>&gt;, <span class="hljs-title">ICollection</span>, <span class="hljs-title">IList</span><br></code></pre></td></tr></table></figure><p>嗯，有个非常有趣的东西 <code>IEnumerable&lt;T&gt;</code>。</p><p>你一定知道 I 表示它是个接口，后面尖括号里面的 T 表示泛型，现在我们来看看它的命名：</p><p><code>Enumerable</code>，可枚举的。字面意思理解，就是这个类里面有一堆东西，你可以一个一个遍历枚举出来（记得 foreach 循环吗？我们经常使用这种枚举来执行 List 操作）。</p><p>而标准 LINQ 的使用前提就是类实现了该接口（或能够隐式转换），很符合直觉，如果你这个类都无法枚举，还怎么查询数据呢？</p><p>在本文的后半，会介绍怎么自己实现这个接口，不过现在我们先记住这个前提，然后来用一下 LINQ 感受一下吧！</p><h2 id="快速开始查询数据"><a href="#快速开始查询数据" class="headerlink" title="快速开始查询数据"></a>快速开始查询数据</h2><p>要开始使用 LINQ，只需一行 using 即可，此时你会发现，对于实现了<code>IEnumerable</code> 的集合来说，可用的方法增加了！</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">using</span> System.Linq;<br>...<br>List&lt;<span class="hljs-built_in">int</span>&gt; numbers = <span class="hljs-keyword">new</span> List&lt;<span class="hljs-built_in">int</span>&gt;() &#123; <span class="hljs-number">1</span>, <span class="hljs-number">114</span>, <span class="hljs-number">514</span>, <span class="hljs-number">233</span>, <span class="hljs-number">322</span>, <span class="hljs-number">44432</span>, <span class="hljs-number">23232</span> &#125;;<br><span class="hljs-keyword">var</span> result = numbers.Take(<span class="hljs-number">2</span>);<br><span class="hljs-comment">// 1</span><br><span class="hljs-comment">// 114</span><br><span class="hljs-keyword">foreach</span> (<span class="hljs-keyword">var</span> number <span class="hljs-keyword">in</span> result)<br>&#123;<br>    Console.WriteLine(number); <br>&#125;<br><span class="hljs-comment">// 23232</span><br>Console.WriteLine(numbers.Last());<br></code></pre></td></tr></table></figure><p>上面的代码，用了 <code>Take</code> <code>Last</code> 这两个 LINQ 方法，顾名思义，take 取出头部的几个元素，last 取得最后单个元素。</p><p>当然 LINQ 的强大之处是使用<strong>方法串链</strong>，一次完成 2 步查询：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">var</span> result = numbers.Take(<span class="hljs-number">2</span>).Last();<br>Console.WriteLine(result); <span class="hljs-comment">// 114</span><br></code></pre></td></tr></table></figure><p>先取出前两个，再取出结果中最后一个！</p><p>怎么样，是不是已经有点<strong>查询</strong>的样子了？你可以自己去看一下 IDE 的智能提示，里面还有许多查询方法可用。</p><h2 id="声明式查询"><a href="#声明式查询" class="headerlink" title="声明式查询"></a>声明式查询</h2><h3 id="基本声明式查询"><a href="#基本声明式查询" class="headerlink" title="基本声明式查询"></a>基本声明式查询</h3><p>但是等等！LINQ 不仅支持通过方法使用（其实这叫做<em>扩展方法</em>，但并不在这篇文章的讨论范围内），还支持<strong>声明式查询</strong>，这是<strong>另一种更加清晰可感的形式</strong>。</p><p>什么意思呢？来看看下面的代码：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs csharp">List&lt;<span class="hljs-built_in">int</span>&gt; numbers = <span class="hljs-keyword">new</span> List&lt;<span class="hljs-built_in">int</span>&gt;() &#123; <span class="hljs-number">1</span>, <span class="hljs-number">114</span>, <span class="hljs-number">514</span>, <span class="hljs-number">233</span>, <span class="hljs-number">322</span>, <span class="hljs-number">44432</span>, <span class="hljs-number">23232</span> &#125;;<br><span class="hljs-keyword">var</span> result = <span class="hljs-keyword">from</span> num <span class="hljs-keyword">in</span> numbers<br>             <span class="hljs-keyword">where</span> num &gt; <span class="hljs-number">500</span><br>             <span class="hljs-keyword">orderby</span> num<br>             <span class="hljs-keyword">select</span> num + <span class="hljs-number">1</span>;<br><span class="hljs-keyword">foreach</span> (<span class="hljs-keyword">var</span> item <span class="hljs-keyword">in</span> result)<br>&#123;<br>    Console.WriteLine(item);<br>&#125;<br></code></pre></td></tr></table></figure><p>把上面的代码键入到 Main 方法尝试一下吧！先根据含义猜测一下会输出什么。</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs text">515<br>23233<br>44433<br></code></pre></td></tr></table></figure><p>猜对了吗？这就是一个非常简单的 <strong>LINQ 声明式查询</strong>示例，这里的查询没有一点<strong>调用方法</strong>的样子！</p><p>现在我们来仔细拆解一下这到底是怎么工作的。我们可以把整个查询拆开成几部分（称为<em>子句</em>）：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">from</span> num <span class="hljs-keyword">in</span> numbers<br></code></pre></td></tr></table></figure><p>先看这一部分。from 子句（数据从哪里来？）首先从要查询的对象里面“取”出数据，存入变量。in （从什么里面取？）指定查询的对象，然后存入 from 后面的变量。这个 num 是一个<strong>范围变量</strong>，有一个范围，里面是多个 int，迭代时会<strong>处理每一个变量</strong>。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">where</span> num &gt; <span class="hljs-number">500</span><br></code></pre></td></tr></table></figure><p>再看这个 where 子句。它的含义就非常清晰好记：过滤出大于 500 的数据。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">orderby</span> num<br></code></pre></td></tr></table></figure><p>再看 orderby 子句。这个子句会对列表内的数据从低到高排序。因为 num 是一个 int 类型，所以可以直接排序。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">select</span> num + <span class="hljs-number">1</span>;<br></code></pre></td></tr></table></figure><p>最后是 select。select 会生成最后<strong>添加到</strong>列表的数据。这里的含义是，把经过前面子句处理的列表中，每个数据加上 1。</p><p>虽然整个查询看起来你都知道在说什么，但是我们还得深入挖一挖，这样你才能写自己的 LINQ 声明式查询。</p><p>首先要明确一点，声明式查询语句，是在声明一种<strong>对应关系</strong>。当查询执行的时候，C# 会<strong>遍历</strong> from 子句中 in 后面的列表，然后对每一个元素执行你的查询。</p><p>我们可以这么理解查询的过程（这只是帮助你理解，不代表 C# 真正的运行逻辑）：</p><ul><li>首先，<code>from num in numbers</code> 遍历取出 numbers 的每一个元素生成临时列表。然后，声明一下，以后每次遍历都创建叫做 num 的变量（也就是令下面的语句，都以 num 来自 numbers 作为前提）</li><li>然后，<code>where num &gt; 500</code> 过滤出大于 500 的数据。刚才提到遍历的变量叫做 num，因此类似 foreach 的循环遍历检查刚才的列表，过滤出结果。</li><li>过滤后的元素列表来到 <code>orderby num</code>。由于是 int 类型，默认数字从低到高排序。</li><li>然后，<code>select num + 1;</code>，遍历刚才排序后的列表中的每一个元素，全部 +1。</li></ul><blockquote><p>where orderby select 都很清晰啊，就是这个 from 也太难记忆了吧！怎么办！？</p></blockquote><p>其实，我们得理解它语义上的含义，这样才好记。</p><p>from，从……来。in，在……里面。from 表示的就是一种从哪里来，到哪里去的<strong>映射关系</strong>。也就是说，告诉 LINQ：这个查询使用 numbers 集合作为数据来源，遍历时各个成员都要用 num 表示。</p><h3 id="分组查询-group"><a href="#分组查询-group" class="headerlink" title="分组查询 group"></a>分组查询 group</h3><p>既然我们已经知道了查询的基础方法，现在可以来点更加深入的东西——比如，LINQ 对查询的结果可以进行分组。先来看看下面的一个 enum 和一个 User 类。</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-built_in">enum</span> Status<br>&#123;<br>    Offline,<br>    Online<br>&#125;<br><span class="hljs-keyword">class</span> <span class="hljs-title">User</span><br>&#123;<br>    <span class="hljs-keyword">public</span> <span class="hljs-built_in">int</span> Id &#123; <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; &#125;<br>    <span class="hljs-keyword">public</span> Status Status &#123; <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>然后，让我们构造一些数据来查询：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs csharp">User[] users = <span class="hljs-keyword">new</span> User[]<br>&#123;<br>    <span class="hljs-keyword">new</span> User &#123; Id = <span class="hljs-number">1</span>, Status = Status.Online &#125;,<br>    <span class="hljs-keyword">new</span> User &#123; Id = <span class="hljs-number">2</span>, Status = Status.Online &#125;,<br>    <span class="hljs-keyword">new</span> User &#123; Id = <span class="hljs-number">3</span>, Status = Status.Offline &#125;,<br>    <span class="hljs-keyword">new</span> User &#123; Id = <span class="hljs-number">4</span>, Status = Status.Online &#125;,<br>&#125;;<br><span class="hljs-keyword">var</span> result=<span class="hljs-keyword">from</span> user <span class="hljs-keyword">in</span> users <span class="hljs-keyword">orderby</span> user.Id<br>           <span class="hljs-keyword">group</span> user <span class="hljs-keyword">by</span> user.Status <span class="hljs-keyword">into</span> userGroup<br>           <span class="hljs-keyword">select</span> userGroup;<br></code></pre></td></tr></table></figure><p>查询的第一行你肯定已经明白了，就是按照 ID 从小到大排序。</p><p>现在我们来拆解这个分组。首先，我们必须明白：这个 var，到底是什么类型呢？</p><p>把鼠标悬浮在上面，IDE 已经给出了答案：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs plaintext">...IEnumerable&lt;out T&gt;<br>T 是 IGrouping&lt;Status, User&gt;<br></code></pre></td></tr></table></figure><p>也就是，分组结果是个 IEnumerable，一个内含多个 IGrouping 的可迭代列表。那么问题来了，里面的元素，IGrouping 又是什么？</p><p>按住 Ctrl 点一下，转到定义：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-comment">// Represents a collection of objects that have a common key.</span><br><span class="hljs-keyword">public</span> <span class="hljs-keyword">interface</span> <span class="hljs-title">IGrouping</span>&lt;<span class="hljs-keyword">out</span> <span class="hljs-title">TKey</span>, <span class="hljs-keyword">out</span> <span class="hljs-title">TElement</span>&gt; : <span class="hljs-title">IEnumerable</span>&lt;<span class="hljs-title">TElement</span>&gt;, <span class="hljs-title">IEnumerable</span><br>&#123;<br>    TKey Key &#123; <span class="hljs-keyword">get</span>; &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>它又是实现了 IEnumerable 的一组数据，里面有一个 Key。</p><p>也就是说，在我们的情境下，IGrouping 是一个<strong>分组</strong>，里面包含<strong>多个 User 元素</strong>，这些 User 元素有<strong>相同的 Key</strong>（也就是相同的 Status，是分组的依据）。明白了这一点，下面的查询就不难理解了：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">group</span> user <span class="hljs-keyword">by</span> user.Status <span class="hljs-keyword">into</span> userGroup<br></code></pre></td></tr></table></figure><p>这个子句的含义是：</p><ul><li>把 user 对应的列表根据 Status 分组，相同 Status 的 User 分到一组</li><li>每个 IGrouping 表示一个分组，包含多个 User 元素</li><li>Status 作为每个 IGrouping 分组的 Key（by 后面的就是 key）</li><li>最后把这些 IGrouping 分组塞到一个新的集合中，叫做 userGroup。</li></ul><p>看看下面的<strong>图片</strong>吧，瞬间秒懂。</p><p><img src="https://img.samhou.top/1769930042879.webp" alt="示意图"></p><p>这下就简单了！我们可以用两层 foreach 循环，来验证一下：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">foreach</span> (<span class="hljs-keyword">var</span> <span class="hljs-keyword">group</span> <span class="hljs-keyword">in</span> result)<br>&#123;<br>    Console.WriteLine(<span class="hljs-string">&quot;Group &quot;</span> + <span class="hljs-keyword">group</span>.Key);<br>    <span class="hljs-keyword">foreach</span> (<span class="hljs-keyword">var</span> item <span class="hljs-keyword">in</span> <span class="hljs-keyword">group</span>)<br>    &#123;<br>        Console.WriteLine(<span class="hljs-string">&quot;User ID #&quot;</span> + item.Id<br>            + <span class="hljs-string">&quot; Status: &quot;</span> + item.Status);<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>结果是：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs csharp">Group Online<br>User ID <span class="hljs-meta">#1 Status: Online</span><br>User ID <span class="hljs-meta">#2 Status: Online</span><br>User ID <span class="hljs-meta">#4 Status: Online</span><br>Group Offline<br>User ID <span class="hljs-meta">#3 Status: Offline</span><br></code></pre></td></tr></table></figure><p>是不是和我们的预想完全一样呢？当然，我们这里是拿 Enum 作为 key，这只是一个比较符合现实、又合理的例子。</p><p>LINQ 会把 <strong>Key 完全相同</strong>的元素分到一组中。所以，你当然可以用其它类型——比如相同的 int 类型，把相同年龄的 User 分到一组（<del>除非有非常好的理由，真的有人会这么干吗？</del>）。在实际编写中，明智地选择 Key 是得到清晰的分组的必要条件。</p><h3 id="合并查询-join"><a href="#合并查询-join" class="headerlink" title="合并查询 join"></a>合并查询 join</h3><p>使用 LINQ 的时候，我们不禁在思考：可不可以让数据来自<strong>多个数据源</strong>呢？这些数据的<strong>某一个属性的值完全一样</strong>，难道就不能把它们合并到一起吗？</p><p>当然是可以的。LINQ 有一个 join 子句，能够帮你达成合并的任务。首先，我们要举两个示例模型。仔细看，确保你理解这两个类的结构和它们的现实含义：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">class</span> <span class="hljs-title">User</span> <span class="hljs-comment">// 这个类没有改</span><br>&#123;<br>    <span class="hljs-keyword">public</span> <span class="hljs-built_in">int</span> Id &#123; <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; &#125;<br>    <span class="hljs-keyword">public</span> Status Status &#123; <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; &#125;<br>&#125;<br><span class="hljs-keyword">class</span> <span class="hljs-title">Message</span> <span class="hljs-comment">// 表示用户发送的一条信息</span><br>&#123;<br>    <span class="hljs-comment">// 发送者 ID</span><br>    <span class="hljs-keyword">public</span> <span class="hljs-built_in">int</span> SenderId &#123;  <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; &#125;<br>    <span class="hljs-comment">// 发送内容</span><br>    <span class="hljs-keyword">public</span> <span class="hljs-built_in">string</span> Text &#123; <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; &#125; = <span class="hljs-string">&quot;&quot;</span>;<br>&#125;<br></code></pre></td></tr></table></figure><p>我们假设这是个用户留言板系统，所有的数据都是合法的，那么，一定会有以下的结论：</p><ul><li>系统中有一个<strong>用户列表</strong>，表示所有注册用户</li><li>还有一个<strong>消息列表</strong>，表示留言板上面的所有消息</li><li>每条消息都<strong>对应</strong>一个用户（一个用户可能发送多条消息）</li></ul><p>那么，我们可以把 User 列表，<strong>合并</strong>到 Message 里面！</p><p>我们先来构造点示例数据，然后展示 join 语句的用法：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><code class="hljs csharp">User[] users = <span class="hljs-keyword">new</span> User[] <span class="hljs-comment">// 不变</span><br>&#123;<br>    <span class="hljs-keyword">new</span> User &#123; Id = <span class="hljs-number">1</span>, Status = Status.Online &#125;,<br>    <span class="hljs-keyword">new</span> User &#123; Id = <span class="hljs-number">2</span>, Status = Status.Online &#125;,<br>    <span class="hljs-keyword">new</span> User &#123; Id = <span class="hljs-number">3</span>, Status = Status.Offline &#125;,<br>    <span class="hljs-keyword">new</span> User &#123; Id = <span class="hljs-number">4</span>, Status = Status.Online &#125;,<br>&#125;;<br>Message[] messages = <span class="hljs-keyword">new</span> Message[] <span class="hljs-comment">// 来构造一个消息列表</span><br>&#123;<br>    <span class="hljs-keyword">new</span> Message &#123;SenderId= <span class="hljs-number">1</span>,Text=<span class="hljs-string">&quot;I love this.&quot;</span>&#125;,<br>    <span class="hljs-keyword">new</span> Message &#123;SenderId= <span class="hljs-number">2</span>,Text=<span class="hljs-string">&quot;No wayyyyy we can leave message&quot;</span> &#125;,<br>    <span class="hljs-keyword">new</span> Message&#123;SenderId=<span class="hljs-number">3</span>,Text=<span class="hljs-string">&quot;OMG this is crazy&quot;</span>&#125;,<br>    <span class="hljs-keyword">new</span> Message&#123;SenderId=<span class="hljs-number">3</span>,Text=<span class="hljs-string">&quot;Great work!&quot;</span>&#125;,<br>    <span class="hljs-keyword">new</span> Message&#123;SenderId=<span class="hljs-number">4</span>,Text=<span class="hljs-string">&quot;Can I delete my message???&quot;</span>&#125;<br>&#125;;<br></code></pre></td></tr></table></figure><p>然后我们来查询：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">var</span> result = <span class="hljs-keyword">from</span> message <span class="hljs-keyword">in</span> messages<br>             <span class="hljs-keyword">join</span> user <span class="hljs-keyword">in</span> users<br>             <span class="hljs-keyword">on</span> message.SenderId <span class="hljs-keyword">equals</span> user.Id<br>             <span class="hljs-keyword">select</span> <span class="hljs-keyword">new</span><br>             &#123;<br>                 SenderId = message.SenderId,<br>                 Text = message.Text,<br>                 UserStatus = user.Status,<br>             &#125;;<br><span class="hljs-keyword">foreach</span> (<span class="hljs-keyword">var</span> item <span class="hljs-keyword">in</span> result)<br>&#123;<br>    Console.WriteLine(<span class="hljs-string">&quot;Message [&quot;</span> + item.Text +<br>        <span class="hljs-string">&quot;] from user #&quot;</span> + item.SenderId +<br>        <span class="hljs-string">&quot; whose status is &quot;</span> + item.UserStatus);<br>&#125;<br></code></pre></td></tr></table></figure><p>嗯，来仔细瞧一瞧，先画个图感受过程：</p><p><img src="https://img.samhou.top/1769935128889.webp" alt="合并示意图"></p><ul><li>from 子句从 messages 里面取出所有消息</li><li>join 子句从 users 列表中取出 user</li><li>如何合并？on … equals … 添加了限制条件。遍历 from 子句生成的临时 message 列表时，会根据条件进行比较。这里的条件是，on 和 equals 后面的内容完全一致</li><li>根据条件，当 user 的 id 和 message 中的 SenderId 相同时，user 和对应的 message 匹配成功。</li><li>合并之后，现在一个元素里面既有 message，又有 user。</li><li>select 子句创建的新的类型，根据之前已经匹配成功得到的临时列表，从每个合并后的元素中遍历取出 message.SenderId message.Text user.Status，然后创建新的对象</li></ul><p>好了，问题来了：我们 select 里面创建的到底是个啥？这个类型我们从来没见过啊？</p><p>没错，我们就是创建的新的类型，但是这个类型根本就没有名字，所有的属性都是只读的，这称作<em>匿名类型</em>。我们一直用 new 来创建新的类型，通常 new 后面需要一个类型名称。当我们省略这个类型名称的时候，我们就创建了一个<strong>匿名类型</strong>。</p><p>你已经知道，我们用 var 来让 C# 自己决定类型，省心省力。实际上，匿名类型也是 var 的重要用法！这是因为，匿名类型<strong>必须使用 var 关键字</strong>来创建变量：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">var</span> testObj = <span class="hljs-keyword">new</span> &#123;<br>    Name = <span class="hljs-string">&quot;Sam&quot;</span>,<br>    Id = <span class="hljs-number">3</span><br>&#125;<br></code></pre></td></tr></table></figure><p>回到 LINQ。来看 select：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">select</span> <span class="hljs-keyword">new</span><br>&#123;<br>    SenderId = message.SenderId,<br>    Text = message.Text,<br>    UserStatus = user.Status<br>&#125;<br></code></pre></td></tr></table></figure><p>这就是一个匿名类型！用 foreach 遍历的时候，我们只能用 var：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs csharp"><span class="hljs-keyword">foreach</span> (<span class="hljs-keyword">var</span> item <span class="hljs-keyword">in</span> result)<br>&#123;<br>    Console.WriteLine(<span class="hljs-string">&quot;Message [&quot;</span> + item.Text +<br>        <span class="hljs-string">&quot;] from user #&quot;</span> + item.SenderId +<br>        <span class="hljs-string">&quot; whose status is &quot;</span> + item.UserStatus);<br>&#125;<br></code></pre></td></tr></table></figure><p>现在我们来运行一下刚才的整个程序：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs text">Message [I love this.] from user #1 whose status is Online<br>Message [No wayyyyy we can leave message] from user #2 whose status is Online<br>Message [OMG this is crazy] from user #3 whose status is Offline<br>Message [Great work!] from user #3 whose status is Offline<br>Message [Can I delete my message???] from user #4 whose status is Online<br></code></pre></td></tr></table></figure><p>完美！这下彻底把两组数据合并了。</p><p>需要特别注意的一点是，join 后面加进来的列表中的数据是会<strong>匹配</strong>的。因此，join 列表中的元素<strong>可能被复制</strong>（一个 user 合并到多条 message），比如上面的 user#3，两条 message 中都有同样的 id 和 status。</p><p>此外，多次使用 join 也是可以的！可以把多个列表合并到一起，此处不再赘述。</p><h2 id="懒计算"><a href="#懒计算" class="headerlink" title="懒计算"></a>懒计算</h2><p>我们刚才一直提到“声明”，其实这和 LINQ 的行为也是一致的。在你<strong>获取</strong>数据结果时，<strong>查询才会真正发生</strong>。看看下面的代码：</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs csharp">List&lt;<span class="hljs-built_in">int</span>&gt; numbers = <span class="hljs-keyword">new</span> List&lt;<span class="hljs-built_in">int</span>&gt;() &#123; <span class="hljs-number">1</span>, <span class="hljs-number">114</span>, <span class="hljs-number">514</span>, <span class="hljs-number">233</span>, <span class="hljs-number">322</span>, <span class="hljs-number">44432</span>, <span class="hljs-number">23232</span> &#125;;<br><span class="hljs-keyword">var</span> result= <span class="hljs-keyword">from</span> num <span class="hljs-keyword">in</span> numbers<br>            <span class="hljs-keyword">where</span> num &gt;=<span class="hljs-number">115</span><br>            <span class="hljs-keyword">orderby</span> num<br>            <span class="hljs-keyword">select</span> num;<br>numbers[<span class="hljs-number">1</span>] = <span class="hljs-number">115</span>;<br><span class="hljs-keyword">foreach</span> (<span class="hljs-keyword">var</span> item <span class="hljs-keyword">in</span> result)<br>&#123;<br>    Console.WriteLine(item); <span class="hljs-comment">// 115 233 322 514 23232 44432</span><br>&#125;<br></code></pre></td></tr></table></figure><p>执行一下，你会发现结果中有一个 115！这明明就是在查询后面才添加到原始的 numbers 列表中才对……为什么会这样呢？</p><p>这是因为，使用 LINQ 的时候，把它赋值给一个变量并不会触发查询，直到赋值的这个变量被用到的时候，才会真正发生查询。这被称为<strong>懒计算</strong>。你在写 LINQ 只是声明了一种查询的<strong>方法</strong>，并非<em>触发了查询</em>。</p><h2 id="预告：Lambda-表达式"><a href="#预告：Lambda-表达式" class="headerlink" title="预告：Lambda 表达式"></a>预告：Lambda 表达式</h2><p>我们之前已经提到，方法和声明式查询<strong>都是标准 LINQ 的一种</strong>。实际上，它们就是<strong>完全一致、可以替换</strong>的关系！</p><p>如果刚才的声明式查让你感到有些疑惑，来换种角度看看LINQ吧。不过，在此之前，我们必须了解一个东西：Lambda 表达式，这样才能写出那些方法。</p><p>但是，这篇文章已经足够长了，因此，我们下次再讨论 lambda 以及 linq，敬请期待！</p>]]>
    </content>
    <id>https://blog.samhou.moe/csharp-linq-guide/</id>
    <link href="https://blog.samhou.moe/csharp-linq-guide/"/>
    <published>2026-02-01T09:03:07.000Z</published>
    <summary>本文以易懂方式介绍C# LINQ，涵盖查询前提IEnumerable、基础查询、声明式语法、分组与合并操作，并说明延迟执行机制。</summary>
    <title>奶奶都能看懂的 C# —— 手把手 LINQ</title>
    <updated>2026-02-01T09:03:07.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Sam Hou</name>
    </author>
    <category term="杂谈" scheme="https://blog.samhou.moe/categories/talking/"/>
    <category term="mjj" scheme="https://blog.samhou.moe/tags/mjj/"/>
    <category term="装机" scheme="https://blog.samhou.moe/tags/%E8%A3%85%E6%9C%BA/"/>
    <category term="看板娘" scheme="https://blog.samhou.moe/tags/%E7%9C%8B%E6%9D%BF%E5%A8%98/"/>
    <category term="oc" scheme="https://blog.samhou.moe/tags/oc/"/>
    <category term="杂谈" scheme="https://blog.samhou.moe/tags/%E6%9D%82%E8%B0%88/"/>
    <content>
      <![CDATA[<p>经常来这个网站拜访的老朋友（<del>真的有吗</del>）都知道，这个博客都是注重深入讲解，聚焦一个主题进行实践的。</p><p>但是！既然是博客，总要有点博客的样子。</p><p>于是，这个杂谈栏目诞生了——《水星冲浪日志》。在这个分类中，我不会涉及很多专业技术知识，尽量让所有人都可以无门槛的看懂（<del>但有些梗和整活可能不行</del>），因此可以说是一个比较轻松的栏目，可以当成不定期更新的报纸一类的东西来看？总之不会深入任何一个话题，放松就好啦，这个栏目和整个网站的其它部分有很大的区别。</p><p>（可以用左边的 RSS 来订阅，也可以扫二维码公众号订阅）</p><p>在开始第一期之前，我打算先来谈谈这个杂谈栏目的名字，<em>水星冲浪日志</em>，到底是什么含义？</p><h2 id="杂谈栏目《水星冲浪日志》"><a href="#杂谈栏目《水星冲浪日志》" class="headerlink" title="杂谈栏目《水星冲浪日志》"></a>杂谈栏目《水星冲浪日志》</h2><p>《水星冲浪日志》这个名字起源于脑海里的一个想法。</p><p>把上网冲浪时候看到有意思的东西记录下来，分享出去——上网冲浪日记。</p><p>但这样也太直白了，必须取一个更加特别，更加好记、有特色的名字。就在这时，阅番目录里面的 <em>ARIA</em>（《水星领航员》）引起了我的注意。</p><p>水星？众所周知，大部分网络论坛都是灌水的好去处，网友们都喜欢灌水……那么，上网冲浪，不就是在水星冲浪了么？</p><p>既然这个栏目也大部分情况下涉及的会是和技术有关的东西，所以我们把日记这个词，再改成更符合技术视角的<strong>日志</strong>——</p><p>完美的命名诞生了！《水星冲浪日志》。</p><p>在这里，希望能留下点什么，记录整个学习、生活的有趣瞬间。这些文字不好分类，因此被归类到日志中……</p><p>关于名字已经说的够多了，让我们开始吧！</p><h2 id="MJJ（买鸡鸡）"><a href="#MJJ（买鸡鸡）" class="headerlink" title="MJJ（买鸡鸡）"></a>MJJ（买鸡鸡）</h2><p><del>沉迷 MJJ 无法自拔。</del></p><p>事情的起因是我觉得博客的评论区太慢了（经常断连），然后希望能够扔掉那台自称三网优化的 2c4g 年抛优惠虚拟主机，扔掉 Azure 和 Digital Ocean 的年抛学生机，找一个至少能稳定活下去的机器，于是注册了<del>万恶之源</del> NodeSeek 账号。</p><p>我的一个朋友给我推荐了 legendvps，但是到手之后发现这家伙线路真的不太行，只能当落地用（现在准备放生了）。</p><p>在论坛混了几天之后，突然像中邪了一样想搞一台加速网站访问的线路机器，要求我的联通宽带能够快速连接。</p><p>然后发了个帖子询问，最终买了台 JP 的的 Softbank 机器。<del>结果第二天就炸鸡了</del>。速度还不错，用起来还是比较爽的。</p><p>于是，MJJ 之旅开始了，现在看到什么机器都想买，即使用不到也要买<del>钱包要不保了</del>。还在家里的老笔记本搭的家里云上部署了一个 N8N 工作流，24 小时 AI 监控 NS 论坛的优惠和感兴趣的帖子。</p><p>感觉完全忘记了初衷……博客的评论区至今还是没有迁移，打算等年抛机快过期的时候，再买个传家宝高性能建站机套上 Edgeone CDN，迁移到那台主机上面去。</p><div class="note info flat"><p><strong>VPS 机器的分类</strong></p><p>按照连接链路，机器分为线路机（访问速度快，有国内优化）、落地机（IP 质量好）；按照网络，有一类特殊的机器叫 NAT 机（多个机器共用 IP，端口需要转发）；按照硬盘大小，大硬盘的叫做大盘鸡；按照用途，用于建站的，稳定、性能强的机器叫做建站机。由于这是个公开的博客，考虑到一些神秘的原因，所以有些细节在文本中无法涉及……总之，这一部分请脑补！</p></div><h2 id="看板娘"><a href="#看板娘" class="headerlink" title="看板娘"></a>看板娘</h2><p>在这篇日志的第二部分，我想来谈谈博客看板娘的角色设定。</p><p>由于设定集是和这篇文章同时上传的，所以在写本文时，这篇设定除了我和一个朋友外没有人看过。虽然我不是文科生，想象力也不太够，但是还是先放在这里：<a href="https://blog.samhou.moe/character/">设定集</a>。你或许也注意到了，现在网站右上角的导航栏关于界面变成了下拉菜单，里面增加了设定集的链接。</p><p>创造这个角色的初衷，是想要拥有自己在网络上的一种虚拟的数字身份（<del>类似 Vtuber</del>），让这个博客在各位客人的脑海里留下更加深刻的印象，同时在博客文章里面用起来增强趣味性。</p><p>于是，这个和我的网名完全一致的 OC（Original Character，原创角色）诞生了，目前作为博客的看板娘存在于设定集中。</p><p>感觉网站头图一直用其它画师的无授权作品实在不太好，虽然她们都比较大方没有找上门，但是我的良心受到了谴责。用 AI 又味道不对，或许最终还得是自己约稿才行吧。</p><p><del>V 点钱给我找画师约稿把看板娘画出来</del></p><h2 id="装机"><a href="#装机" class="headerlink" title="装机"></a>装机</h2><p>在这篇杂谈的最后，来说说一点最近的新突破吧！</p><p>起因是家里有一台超级古董的惠普一体机，淘汰下来吃灰好几年了。</p><p>于是，作为计科的学生，我决定不止停留在软件开发上，还要去学习硬件知识。然后就把这台一体机拆成碎片了……</p><p>电动手枪钻真的是世界上最伟大的发明！我这种<del>肌无力的</del>人都可以轻松拧开任何螺丝。</p><p><img src="https://img.samhou.top/1768881226731.webp" alt="碎片"></p><p>从中回收了 CPU，是 i3-4170t。</p><p>拆开了研究清楚了，但是接下来的任务就是动手 DIY 一台新的机器。现在硬件（内存、硬盘）价格都飙到天上去了，况且我几年前的高性能品牌 PC 还没有出现任何问题，所以新买一整套完全没有必要且不现实。因此，这次装机单纯是学习用途，能点亮能用即为成功，关键是为以后的 DIY 积累经验。</p><p>在一个老友的指导下，创建了以下装机单：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs text">cpu i3 4170t 0r 旧电脑拆机<br>主板华硕h81m-k 二手65r<br>内存金士顿3代4g*2 二手共40r<br>散热讯钛 17.2r<br>电源长城200w 二手23r<br>硬盘500g辣鸡机械 0r 旧电脑拆机<br>工业风格机箱 21.9r<br>硅脂 5.16r vga线 4.9r 电源线 8.53r sata线 1r<br>显示器辣鸡老显示器 键盘古董米物 鼠标雷柏太大了手控制不了闲置鼠标 全部0r<br></code></pre></td></tr></table></figure><p>然后就是静静地等待到货了，这台练手机器甚至只需要 200 块。</p><p>装机过程还是比较顺利的，看了硬件茶谈的 3D 演示视频，没有出什么岔子。然后就是激动人心的亮机时刻——</p><p><img src="https://img.samhou.top/1768882687547.webp" alt="放一会局域网 Jellyfin 试试看"></p><p>这价位，当个影音娱乐小主机还不错，打游戏还是别想了……</p><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>总之，这就是一个轻松愉悦的杂谈栏目，什么都会写，我也不太会局限于技术，会天马行空一点。那么，下期《水星冲浪日志》再会！</p>]]>
    </content>
    <id>https://blog.samhou.moe/aqua-surf-1/</id>
    <link href="https://blog.samhou.moe/aqua-surf-1/"/>
    <published>2026-01-20T04:01:30.000Z</published>
    <summary>《水星冲浪日志》的第一期，记录了这个杂谈栏目的起源，博主初探 mjj 的过程，看板娘的前世今生，以及学习装机的全过程。</summary>
    <title>水星冲浪日志 1 —— 杂谈、MJJ、看板娘和装机</title>
    <updated>2026-01-20T04:01:30.000Z</updated>
  </entry>
</feed>
