大风吹浪 發表於 2026-1-7 10:02:03

三种在C++中高效获取日志文件最后10行的方法

<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li>准备工作:创建一个测试文件testlog.txt</li><li>第一部分:方法 1 (&ldquo;天真&rdquo;法) &mdash;&mdash; 读取所有行</li><li>第二部分:方法 2 (&ldquo;折中&rdquo;法) &mdash;&mdash; 循环缓冲区</li><li>第三部分:方法 3 (&ldquo;专业&rdquo;法) &mdash;&mdash;seekg反向读取</li><li>第四部分:&ldquo;X光透 视&rdquo;&mdash;&mdash;亲眼目睹&ldquo;反向搜寻&rdquo;</li><ul class="second_class_ul"><li>&ldquo;X光&rdquo;实战(基于seekg_pro.cpp)</li></ul><li>动手试试!(终极挑战:你的&ldquo;可配置tail&rdquo;)</li><ul class="second_class_ul"></ul></ul></div><p>在C++编程中,你经常需要处理文件,尤其是日志文件。一个非常常见的任务是:&ldquo;我不想看整个10GB的日志文件,我只想看<strong>最后 10 行</strong>,看看最近发生了什么。&rdquo;</p>
<p>这就像 Linux/macOS 上的 <code>tail -n 10</code> 命令。</p>
<p><strong>一个简单的比喻:&ldquo;读一本厚书的最后一章&rdquo;</strong></p>
<ul><li><strong>问题:</strong> 你想读一本 1000 页巨著的最后一章(最后10行)。</li><li><strong>&ldquo;天真&rdquo;的办法 (Naive Method)</strong>:你从第 1 页开始,一页一页地<strong>读并记住</strong>(存入内存)<strong>所有</strong> 1000 页内容,最后再翻到你记住的第 990 页开始看。<ul><li><strong>缺点:</strong> 极度<strong>浪费内存</strong>(O(N) 空间)和<strong>时间</strong>(O(N) 时间)。</li></ul></li><li><strong>&ldquo;折中&rdquo;的办法 (Circular Buffer)</strong>:你只拿 10 张便签。你从第 1 页开始读,第 1 页内容写在便签1,&hellip;,第 10 页写在便签10。当你读第 11 页时,你<strong>擦掉</strong>便签1,写上第 11 页的内容。读第 12 页时,<strong>擦掉</strong>便签2&hellip;<ul><li><strong>缺点:</strong> 你<strong>仍然</strong>需要从头到尾读完 1000 页(O(N) 时间)。</li><li><strong>优点:</strong> 你只需要 10 张便签的内存(O(k) 空间)。</li></ul></li><li><strong>&ldquo;专业&rdquo;的办法 (Seek from End)</strong>:你直接把书<strong>翻到最后一页</strong>(<code>seekg(0, ios::end)</code>)。然后,你开始<strong>一页一页往前翻</strong>,一边翻一边<strong>数</strong>你翻过了多少个&ldquo;章节末尾&rdquo;(<code>\n</code> 换行符)。当你数到 10 个时,你就停下,然后从这里<strong>往后</strong>读到结尾。<ul><li><strong>优点:</strong> <strong>速度极快</strong>(只读文件尾部,O(k*L) 时间,L为行长),<strong>几乎不占内存</strong>(O(1) 空间)。</li><li><strong>缺点:</strong> 逻辑最复杂。</li></ul></li></ul>
<p><strong>在本教程中,你将学会:</strong></p>
<ul><li><strong>文件输入流 <code>ifstream</code></strong>:如何打开和读取文件。</li><li><strong>方法 1 (&ldquo;天真&rdquo;法)</strong>:读取所有行到 <code>vector</code>。</li><li><strong>方法 2 (&ldquo;折中&rdquo;法)</strong>:使用 <code>deque</code> (双端队列) 作为&ldquo;循环缓冲区&rdquo;。</li><li><strong>方法 3 (&ldquo;专业&rdquo;法)</strong>:使用 <code>seekg</code> 和 <code>tellg</code> 从文件末尾反向读取。</li><li><strong>文件指针 (<code>seekg</code>)</strong>:<code>ios::end</code>, <code>ios::cur</code> 的含义。</li><li><strong>&ldquo;X光透 视&rdquo;</strong>:用调试器&ldquo;亲眼目睹&rdquo; <code>seekg</code> 是如何反向计数的。</li></ul>
<p><strong>前置知识说明 (100% 自洽):</strong></p>
<ul><li><strong>变量 (Variable)</strong>:理解存储数据的&ldquo;盒子&rdquo;,如 <code>int n = 10;</code>。</li><li><strong><code>string</code> (字符串)</strong>:C++标准库提供的&ldquo;魔法弹性盒子&rdquo;,用于处理文本。你需要 <code>#include &lt;string&gt;</code>。</li><li><strong><code>vector</code> (向量)</strong>:C++标准库提供的一种&ldquo;动态数组&rdquo;(&ldquo;魔法弹性盒子列表&rdquo;)。你需要 <code>#include &lt;vector&gt;</code>。</li><li><strong><code>deque</code> (双端队列)</strong>:类似 <code>vector</code>,但支持<strong>高效地</strong>在<strong>头部</strong>和尾部添加/删除元素。你需要 <code>#include &lt;deque&gt;</code>。</li><li><strong><code>ifstream</code> (文件输入流)</strong>:C++ 用于<strong>读取</strong>文件的工具。你需要 <code>#include &lt;fstream&gt;</code>。</li><li><strong><code>seekg</code> / <code>tellg</code></strong>:<code>ifstream</code> 的成员函数,用于&ldquo;<strong>S</strong>eek <strong>G</strong>et&rdquo; (移动读取指针) 和 &ldquo;<strong>T</strong>ell <strong>G</strong>et&rdquo; (告知读取指针位置)。</li><li><strong>编译 (Compile)</strong>:C++代码(&ldquo;食谱&rdquo;)必须被&ldquo;编译&rdquo;(&ldquo;烘焙&rdquo;),才能变成电脑可执行的程序(&ldquo;蛋糕&rdquo;)。</li></ul>
<p class="maodian"></p><h2>准备工作:创建一个测试文件testlog.txt</h2>
<p>在运行代码前,请在你的 <code>.cpp</code> 文件<strong>相同</strong>的目录下,创建一个名为 <code>testlog.txt</code> 的文件,并填入以下内容(确保最后一行有换行):</p>
<div class="jb51code"><pre class="brush:cpp;">Line 1: The quick brown fox
Line 2: jumps over
Line 3: the lazy dog.
Line 4: ---
Line 5: C++ File I/O
Line 6: is powerful.
Line 7: ---
Line 8: Testing line 8.
Line 9: Testing line 9.
Line 10: Testing line 10.
Line 11: Testing line 11.
Line 12: Testing line 12.
Line 13: This is the final line.
</pre></div>
<p class="maodian"></p><h2>第一部分:方法 1 (&ldquo;天真&rdquo;法) &mdash;&mdash; 读取所有行</h2>
<p><strong>逻辑:</strong> 把文件的<strong>每一行</strong>都读入一个 <code>vector&lt;string&gt;</code>,然后只打印这个 <code>vector</code> 的最后 10 个元素。</p>
<p><strong><code>naive_tail.cpp</code></strong></p>
<div class="jb51code"><pre class="brush:cpp;">#include &lt;iostream&gt;
#include &lt;fstream&gt;
#include &lt;string&gt;
#include &lt;vector&gt;
using namespace std;

void printLast10_Naive(const string&amp; filename) {
    ifstream file(filename);
    if (!file.is_open()) {
      cerr &lt;&lt; "错误: 无法打开文件 " &lt;&lt; filename &lt;&lt; endl;
      return;
    }

    vector&lt;string&gt; allLines;
    string line;
   
    // 1. “天真”地读取 *所有* 行
    while (getline(file, line)) {
      allLines.push_back(line);
    }
    file.close();

    // 2. 计算从哪里开始打印
    int totalLines = allLines.size();
    int start_index = 0;
    if (totalLines &gt; 10) {
      start_index = totalLines - 10;
    }

    // 3. 打印最后 10 (或更少) 行
    cout &lt;&lt; "--- 方法 1 (Naive) ---" &lt;&lt; endl;
    for (int i = start_index; i &lt; totalLines; ++i) {
      cout &lt;&lt; allLines &lt;&lt; endl;
    }
}

int main() {
    printLast10_Naive("testlog.txt");
    return 0;
}
</pre></div>
<ul><li><strong>优点:</strong> 逻辑最简单,易于理解。</li><li><strong>缺点:</strong> <strong>极度</strong>浪费内存。如果 <code>testlog.txt</code> 是 10GB,你的程序会尝试申请 10GB 内存!</li></ul>
<p class="maodian"></p><h2>第二部分:方法 2 (&ldquo;折中&rdquo;法) &mdash;&mdash; 循环缓冲区</h2>
<p><strong>逻辑:</strong> 我们只保留一个<strong>固定大小</strong>(10)的&ldquo;缓冲区&rdquo;(使用 <code>deque</code>)。从头到尾读取文件,每读一行,就把它<strong>塞进</strong>缓冲区,如果缓冲区&ldquo;满了&rdquo;(超过10),就从<strong>前面</strong>&ldquo;<strong>挤掉</strong>&rdquo;最旧的那一行。</p>
<p><strong><code>circular_buffer.cpp</code></strong></p>
<div class="jb51code"><pre class="brush:cpp;">#include &lt;iostream&gt;
#include &lt;fstream&gt;
#include &lt;string&gt;
#include &lt;deque&gt; // 需要双端队列
using namespace std;

void printLast10_Circular(const string&amp; filename, int N = 10) {
    ifstream file(filename);
    if (!file.is_open()) {
      cerr &lt;&lt; "错误: 无法打开文件 " &lt;&lt; filename &lt;&lt; endl;
      return;
    }

    deque&lt;string&gt; buffer;
    string line;

    // 1. 仍然读取 *所有* 行
    while (getline(file, line)) {
      // 2. 添加到“队尾”
      buffer.push_back(line);
      
      // 3. 如果缓冲区“超载”,从“队首”挤掉
      if (buffer.size() &gt; N) {
            buffer.pop_front();
      }
    }
    file.close();

    // 4. 打印缓冲区中剩下的 N 行
    cout &lt;&lt; "--- 方法 2 (Circular Buffer) ---" &lt;&lt; endl;
    for (const string&amp; s : buffer) {
      cout &lt;&lt; s &lt;&lt; endl;
    }
}

int main() {
    printLast10_Circular("testlog.txt");
    return 0;
}
</pre></div>
<ul><li><strong>优点:</strong> 内存效率极高(O(k) 空间)。</li><li><strong>缺点:</strong> 仍然需要从头到尾读取整个文件(O(N) 时间),对于 10GB 的文件,这仍然很慢。</li></ul>
<p class="maodian"></p><h2>第三部分:方法 3 (&ldquo;专业&rdquo;法) &mdash;&mdash;seekg反向读取</h2>
<p><strong>逻辑:</strong> 像&ldquo;<code>tail</code> 命令&rdquo;一样,直接跳到文件末尾,然后<strong>一个字节一个字节地往前&ldquo;挪&rdquo;</strong>,同时**&ldquo;数&rdquo;**换行符 <code>\n</code>。当我们数到 10 个换行符时,我们就找到了第 10 行的开头。</p>
<p><strong><code>seekg_pro.cpp</code> (推荐的方式)</strong></p>
<div class="jb51code"><pre class="brush:cpp;">#include &lt;iostream&gt;
#include &lt;fstream&gt;
#include &lt;string&gt;
using namespace std;

void printLast10_Pro(const string&amp; filename, int N = 10) {
    ifstream file(filename);
    if (!file.is_open()) {
      cerr &lt;&lt; "错误: 无法打开文件 " &lt;&lt; filename &lt;&lt; endl;
      return;
    }

    // 1. 跳转到文件末尾
    //    (ios::ate 模式可以打开文件并立即定位到末尾)
    //    或者使用 seekg:
    file.seekg(0, ios::end);

    // 2. 获取当前位置(即文件总大小)
    long long pos = file.tellg();
   
    // 如果文件为空
    if (pos == 0) {
      cout &lt;&lt; "文件为空。" &lt;&lt; endl;
      return;
    }
   
    int newlineCount = 0;
    string lineBuffer; // 用于读取最后的残行

    // 3. “行内预警”:我们从 *最后一个字符* 开始往前“跳”
    // (pos 是文件大小,最后一个字符的索引是 pos - 1)
    for (long long i = pos - 1; i &gt;= 0; i--) {
      file.seekg(i); // “跳”到第 i 个字节
      
      char c = file.get(); // 读取那 1 个字节
      
      if (c == '\n') {
            newlineCount++;
      }
      
      // 4. “刹车”:当我们找到 N 个换行符时
      // (注意:GFG的例子是 == N,但 &gt;= N 更健壮)
      if (newlineCount &gt;= N) {
            // “行内预警”:我们需要跳到 *这个换行符之后* 的位置
            file.seekg(i + 1);
            break; // 停止“回溯”
      }
    }
   
    // 5. 如果文件行数不足 N,我们最终会跳到开头
    if (newlineCount &lt; N) {
      file.seekg(0); // 重置到文件开头
    }

    // 6. 现在,从我们“停下”的位置,*顺序* 读到文件末尾
    cout &lt;&lt; "--- 方法 3 (Seek from End) ---" &lt;&lt; endl;
    string line;
    while (getline(file, line)) {
      cout &lt;&lt; line &lt;&lt; endl;
    }

    file.close();
}

int main() {
    printLast10_Pro("testlog.txt");
    return 0;
}
</pre></div>
<p><strong>&ldquo;手把手&rdquo;终端模拟 (所有方法):</strong></p>
<div class="jb51code"><pre class="brush:cpp;">PS C:\MyCode&gt; g++ ... # 编译所有 .cpp 文件
PS C:\MyCode&gt; .\naive_tail.exe
--- 方法 1 (Naive) ---
Line 4: ---
Line 5: C++ File I/O
Line 6: is powerful.
Line 7: ---
Line 8: Testing line 8.
Line 9: Testing line 9.
Line 10: Testing line 10.
Line 11: Testing line 11.
Line 12: Testing line 12.
Line 13: This is the final line.

PS C:\MyCode&gt; .\circular_buffer.exe
--- 方法 2 (Circular Buffer) ---
Line 4: ---
... (输出同上) ...
Line 13: This is the final line.

PS C:\MyCode&gt; .\seekg_pro.exe
--- 方法 3 (Seek from End) ---
Line 4: ---
... (输出同上) ...
Line 13: This is the final line.
</pre></div>
<p><strong>顿悟时刻:</strong> 三种方法<strong>结果相同</strong>,但<strong>效率</strong>(尤其是内存和I/O)<strong>天差地别</strong>!<code>seekg</code> 是处理大文件的&ldquo;专业&rdquo;选择。</p>
<p class="maodian"></p><h2>第四部分:&ldquo;X光透 视&rdquo;&mdash;&mdash;亲眼目睹&ldquo;反向搜寻&rdquo;</h2>
<p>让我们用&ldquo;X光眼镜&rdquo;(调试器)来观察 <code>seekg_pro.cpp</code> 是如何工作的。</p>
<p class="maodian"></p><h3>&ldquo;X光&rdquo;实战(基于seekg_pro.cpp)</h3>
<p><strong>设置断点:</strong></p>
<ul><li><strong>动作:</strong> 在VS Code中,把你的鼠标移动到<strong>第32行</strong>(<code>if (c == &#39;\n&#39;)</code> 那一行)的<strong>行号左边</strong>。</li><li><strong>点击</strong>那个小 red dot,设置一个<strong>断点</strong>。</li></ul>
<p><strong>启动&ldquo;子弹时间&rdquo;(F5):</strong></p>
<ul><li><strong>动作:</strong> 按下 <code>F5</code> 键。</li><li><strong>你会看到:</strong> <code>file</code> 被打开,<code>seekg(0, ios::end)</code> 被执行,<code>pos</code> 被设为文件大小(例如 250 字节)。</li></ul>
<p><strong>第一次&ldquo;冻结&rdquo; (i = 249, 假设):</strong></p>
<ul><li><code>for</code> 循环开始。<code>i</code> 是 249 (文件的最后一个字符)。</li><li><code>file.seekg(249)</code> 执行。</li><li><code>file.get()</code> 读取 <code>testlog.txt</code> 的最后一个字符(假设是 <code>\n</code>)。</li><li>程序&ldquo;冻结&rdquo;在第32行。</li><li><strong>开启&ldquo;X光&rdquo;(观察变量):</strong><ul><li><code>pos: 250</code></li><li><code>i: 249</code></li><li><code>newlineCount: 0</code></li><li><code>c: &#39;\n&#39;</code></li></ul></li><li><strong>动作:</strong> <strong>按下 <code>F10</code> 键</strong>(&ldquo;Step Over&rdquo;,步过)。</li><li><strong>你会看到:</strong> <code>newlineCount</code> 变成了 <code>1</code>。</li></ul>
<p><strong>继续执行 (F5):</strong></p>
<ul><li><strong>动作:</strong> <strong>连续按下 <code>F5</code> 键</strong>(&ldquo;Continue&rdquo;,让程序在断点处循环)。</li><li><strong>你会看到:</strong> 调试器会<strong>一次又一次</strong>地停在<strong>第32行</strong>。</li><li><strong>观察 <code>i</code> 和 <code>newlineCount</code> 的变化:</strong><ul><li><code>i</code> 在<strong>递减</strong> (248, 247, &hellip;)。</li><li>只有当 <code>c</code> <strong>恰好</strong>是 <code>\n</code> 时,<code>newlineCount</code> 才会<strong>增加</strong>。</li></ul></li><li>&hellip;</li><li><strong>第十次&ldquo;冻结&rdquo;在 <code>\n</code>:</strong><ul><li>假设 <code>i</code> 此时是 60。</li><li><code>c: &#39;\n&#39;</code>。</li><li><code>newlineCount</code> 变成了 <code>9</code>。</li></ul></li><li><strong>动作:</strong> 按下 <code>F10</code> 键,<code>newlineCount</code> 变为 <code>10</code>。</li><li><strong>动作:</strong> 再按 <code>F10</code> 键,<code>if (newlineCount &gt;= 10)</code> <strong>为 <code>true</code></strong>!</li><li><strong>动作:</strong> <strong>按下 <code>F11</code> 键(&ldquo;Step Into&rdquo;)</strong> 进入 <code>if</code> 块。</li><li><strong>你会看到:</strong> 高亮条移动到 <code>file.seekg(i + 1);</code> (即 <code>file.seekg(61);</code>)。</li><li><strong>动作:</strong> 按 <code>F10</code> 执行 <code>break;</code>。</li><li><strong>顿悟时刻:</strong> 循环终止!程序&ldquo;定位&rdquo;到了第10个换行符(索引60)。</li></ul>
<p><strong>(程序继续)</strong> <code>file.seekg(61)</code> 将指针设置到&ldquo;第4行&rdquo;的开头,<code>while (getline(...))</code> 开始顺序打印,直到文件末尾。</p>
<p class="maodian"></p><h2>动手试试!(终极挑战:你的&ldquo;可配置tail&rdquo;)</h2>
<p>现在,你来当一次&ldquo;工具开发者&rdquo;。</p>
<p><strong>任务:</strong></p>
<ol><li><strong>复制</strong>本教程&ldquo;<strong>方法 2 (循环缓冲区)</strong>&rdquo;的代码(<code>printLast10_Circular</code>)。</li><li><strong>修改</strong>这个函数,使其能够<strong>返回</strong>一个 <code>vector&lt;string&gt;</code>,而不是 <code>void</code>(打印)。</li><li>在 <code>main</code> 函数中,调用这个新函数(比如 <code>vector&lt;string&gt; lastLines = getLastNLines(&quot;testlog.txt&quot;, 5);</code>),并<strong>自己</strong>遍历打印这个返回的 <code>vector</code>。</li><li><strong>(进阶)</strong> <strong>复制</strong>本教程&ldquo;<strong>方法 3 (专业 <code>seekg</code>)</strong>&rdquo;的代码,并<strong>同样</strong>将其修改为<strong>返回 <code>vector&lt;string&gt;</code></strong>,而不是 <code>void</code>(打印)。(提示:在 <code>file.seekg(pos);</code> 之后,你需要使用 <code>getline</code> 循环把剩余的行读入一个新的 <code>vector</code> 并返回)。</li></ol>
<p><strong><code>flexible_tail.cpp</code> (你的 TODO - 挑战方法 2):</strong></p>
<div class="jb51code"><pre class="brush:cpp;">#include &lt;iostream&gt;
#include &lt;fstream&gt;
#include &lt;string&gt;
#include &lt;deque&gt;
#include &lt;vector&gt;
using namespace std;

// --- TODO 1 &amp; 2: 修改函数,使其返回 vector&lt;string&gt; ---
vector&lt;string&gt; getLastNLines_Circular(const string&amp; filename, int N = 10) {
    ifstream file(filename);
    deque&lt;string&gt; buffer;
   
    // (如果打开失败,返回一个空 vector)
    if (!file.is_open()) {
      cerr &lt;&lt; "错误: 无法打开文件 " &lt;&lt; filename &lt;&lt; endl;
      return vector&lt;string&gt;();
    }
   
    string line;
    while (getline(file, line)) {
      buffer.push_back(line);
      if (buffer.size() &gt; N) {
            buffer.pop_front();
      }
    }
    file.close();

    // --- TODO 2: 将 deque 转换为 vector 并返回 ---
    // (提示:vector 有一个构造函数可以直接接收两个迭代器)
    // return vector&lt;string&gt;(buffer.begin(), buffer.end());
}

int main() {
    // --- TODO 3: 调用新函数并打印 ---
    cout &lt;&lt; "--- 测试 getLastNLines (N=5) ---" &lt;&lt; endl;
   
    // vector&lt;string&gt; last5Lines = getLastNLines_Circular("testlog.txt", 5);
   
    // for (const string&amp; s : last5Lines) {
    //   cout &lt;&lt; s &lt;&lt; endl;
    // }
   
    return 0;
}
</pre></div>
<blockquote><p>这个挑战让你把&ldquo;打印&rdquo;逻辑和&ldquo;数据获取&rdquo;逻辑分离开,这是更健壮的函数设计。如果你能进一步挑战并修改 seekg 版本,你就能完全掌握C++中高效文件读取的精髓!</p></blockquote>
<p>以上就是三种在C++中高效获取日志文件最后10行的方法的详细内容,更多关于C++获取日志文件最后10行的资料请关注琼殿技术社区其它相关文章!</p>
                           
                            <div class="art_xg">
                              <b>您可能感兴趣的文章:</b><ul><li>C++简单日志系统实现代码示例</li><li>c++日志库log4cplus快速入门小结</li><li>C++ Log4cpp跨平台日志库的使用小结</li><li>C++多进程环境下的日志管理策略和最佳实践</li><li>C++中实现调试日志输出</li></ul>
                            </div>

                        </div>
                        <!--endmain-->
頁: [1]
查看完整版本: 三种在C++中高效获取日志文件最后10行的方法