《现代 C++ 手把手实现 RPN 计算器》
- 版本:1.0
- 编译器:GCC 10+ / Clang 12+ / MSVC 2019+
- 标准:C++20
目录
- 第 1 章:Hello RPN
- 第 2 章:头文件化 + 单元测试
- 第 3 章:交互式 REPL
- 第 4 章:单文件发布 & 交叉编译
- 第 5 章:课后挑战
第 1 章:Hello RPN
先给出一个最小可运行版本,保存为 rpn_min.cpp。
rpn_min.cpp
#include <iostream>
#include <sstream>
#include <stack>
#include <stdexcept>
double rpn(const std::string& line){
std::stack<double> st;
std::istringstream iss(line);
std::string tok;
while (iss >> tok){
if (tok == "+" || tok == "-" || tok == "*" || tok == "/"){
if (st.size() < 2) throw std::runtime_error("need 2 operands");
double b = st.top(); st.pop();
double a = st.top(); st.pop();
switch (tok[0]){
case '+': st.push(a + b); break;
case '-': st.push(a - b); break;
case '*': st.push(a * b); break;
case '/':
if (b == 0) throw std::runtime_error("divide by 0");
st.push(a / b); break;
}
} else {
st.push(std::stod(tok));
}
}
if (st.size() != 1) throw std::runtime_error("stack size != 1");
return st.top();
}
int main(){
std::cout << "RPN> ";
std::string line;
std::getline(std::cin, line);
try {
std::cout << "Result: " << rpn(line) << '\n';
} catch (const std::exception& ex){
std::cerr << "Error: " << ex.what() << '\n';
}
}
编译 & 运行:
第 2 章:头文件化 + 单元测试
rpn_engine.hpp
#pragma once
#include <stack>
#include <sstream>
#include <stdexcept>
#include <string>
double evaluateRPN(const std::string& expr){
std::stack<double> st;
std::istringstream iss(expr);
std::string tok;
while (iss >> tok){
if (tok.size() == 1 && std::string("+-*/").find(tok[0]) != std::string::npos){
if (st.size() < 2) throw std::runtime_error("stack underflow");
double b = st.top(); st.pop();
double a = st.top(); st.pop();
switch (tok[0]){
case '+': st.push(a + b); break;
case '-': st.push(a - b); break;
case '*': st.push(a * b); break;
case '/':
if (b == 0) throw std::runtime_error("divide by zero");
st.push(a / b); break;
}
} else {
st.push(std::stod(tok));
}
}
if (st.size() != 1) throw std::runtime_error("single value expected");
return st.top();
}
test.cpp
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include "doctest/doctest.h"
#include "rpn_engine.hpp"
TEST_CASE("basic"){
CHECK(evaluateRPN("3 4 +") == 7);
CHECK(evaluateRPN("2 3 4 + *") == 14);
CHECK(evaluateRPN("10 2 /") == 5);
}
TEST_CASE("exceptions"){
CHECK_THROWS(evaluateRPN("1 +"));
CHECK_THROWS(evaluateRPN("3 0 /"));
CHECK_THROWS(evaluateRPN("1 2 3"));
}
下载 doctest 单文件头库并执行测试:
第 3 章:交互式 REPL
借助 linenoise 单文件头库实现行编辑与历史。
repl.cpp
#include "rpn_engine.hpp"
#include "linenoise.hpp"
#include <iomanip>
int main(){
const char* prompt = "rpn> ";
std::string line;
while (linenoise::Readline(prompt, line)){
if (line == "exit" || line == "quit") break;
try {
double v = evaluateRPN(line);
std::cout << "= " << std::setprecision(12) << v << '\n';
linenoise::AddHistory(line.c_str());
} catch (const std::exception& ex){
std::cerr << "error: " << ex.what() << '\n';
}
}
}
第 4 章:单文件发布 & 交叉编译
把全部源码合并成 rpn_single.cpp,-Os -s -DNDEBUG 优化后仅 ~92 KB(UPX 后)。
Linux→Windows 交叉编译
第 5 章:课后挑战
- 增加幂运算 (
^) 与单目负号
- 支持变量存储 (
sto / rcl)
- 用
std::variant 实现高精度有理数,避免浮点误差
- 编译为 WebAssembly,在浏览器里运行!
祝 hacking 愉快!
import std/[strutils, os, sequtils]
import nimib
import ./lib/codeOutput
var nbToc: NbBlock
template addToc =
newNbBlock("nbText", false, nb, nbToc, ""):
nbToc.output = "## 目录\n\n"
template nbNewSection(name: string) =
let anch = name.toLower.replace(" ", "-")
nbText: "<a name=\"" & anch & "\"></a>\n<br>\n### " & name & "\n\n---"
nbToc.output.add "1. <a href=\"#" & anch & "\">" & name & "</a>\n"
template nbSubSection(name: string) =
let anch = name.toLower.replace(" ", "-")
nbText: "<a name=\"" & anch & "\"></a>\n<br>\n#### " & name & "\n\n"
nbToc.output.add " * <a href=\"#" & anch & "\">" & name & "</a>\n"
template initCodeTheme* =
nb.context["stylesheet"] = """
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/water.css@2/out/water.css">"""
nb.context["highlight"] = """
<link rel="stylesheet" media="(prefers-color-scheme: dark)"
href="https://cdn.jsdelivr.net/gh/highlightjs/highlight.js/src/styles/github-dark.css">
<link rel="stylesheet" media="(prefers-color-scheme: light)"
href="https://cdn.jsdelivr.net/gh/highlightjs/highlight.js/src/styles/github.css">"""
nbRawHtml: """
<style>
pre code{border-radius:6px}pre{border:1px solid #809eb7;border-radius:6px 6px 0 0;margin-bottom:0}
pre.nb-output{border:1px solid #809eb7;border-radius:0 0 6px 6px;border-top:none;padding:.8em;margin-top:0;overflow-x:auto;background:#f8f8f8}
</style>"""
nbInit()
initCodeTheme()
nbText: """
# 《现代 C++ 手把手实现 RPN 计算器》
- 版本:1.0
- 编译器:GCC 10+ / Clang 12+ / MSVC 2019+
- 标准:C++20
"""
addToc()
nbNewSection("第 1 章:Hello RPN")
nbText: """
先给出一个**最小可运行**版本,保存为 `rpn_min.cpp`。
"""
nbSubSection("rpn_min.cpp")
nbRawHtml: """
<pre><code class="language-cpp">#include <iostream>
#include <sstream>
#include <stack>
#include <stdexcept>
double rpn(const std::string& line){
std::stack<double> st;
std::istringstream iss(line);
std::string tok;
while (iss >> tok){
if (tok == "+" || tok == "-" || tok == "*" || tok == "/"){
if (st.size() < 2) throw std::runtime_error("need 2 operands");
double b = st.top(); st.pop();
double a = st.top(); st.pop();
switch (tok[0]){
case '+': st.push(a + b); break;
case '-': st.push(a - b); break;
case '*': st.push(a * b); break;
case '/':
if (b == 0) throw std::runtime_error("divide by 0");
st.push(a / b); break;
}
} else {
st.push(std::stod(tok));
}
}
if (st.size() != 1) throw std::runtime_error("stack size != 1");
return st.top();
}
int main(){
std::cout << "RPN> ";
std::string line;
std::getline(std::cin, line);
try {
std::cout << "Result: " << rpn(line) << '\n';
} catch (const std::exception& ex){
std::cerr << "Error: " << ex.what() << '\n';
}
}
</code></pre>
"""
nbText: """
编译 & 运行:
"""
nbNewSection("第 2 章:头文件化 + 单元测试")
nbSubSection("rpn_engine.hpp")
nbRawHtml: """
<pre><code class="language-cpp">#pragma once
#include <stack>
#include <sstream>
#include <stdexcept>
#include <string>
double evaluateRPN(const std::string& expr){
std::stack<double> st;
std::istringstream iss(expr);
std::string tok;
while (iss >> tok){
if (tok.size() == 1 && std::string("+-*/").find(tok[0]) != std::string::npos){
if (st.size() < 2) throw std::runtime_error("stack underflow");
double b = st.top(); st.pop();
double a = st.top(); st.pop();
switch (tok[0]){
case '+': st.push(a + b); break;
case '-': st.push(a - b); break;
case '*': st.push(a * b); break;
case '/':
if (b == 0) throw std::runtime_error("divide by zero");
st.push(a / b); break;
}
} else {
st.push(std::stod(tok));
}
}
if (st.size() != 1) throw std::runtime_error("single value expected");
return st.top();
}
</code></pre>
"""
nbSubSection("test.cpp")
nbRawHtml: """
<pre><code class="language-cpp">#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include "doctest/doctest.h"
#include "rpn_engine.hpp"
TEST_CASE("basic"){
CHECK(evaluateRPN("3 4 +") == 7);
CHECK(evaluateRPN("2 3 4 + *") == 14);
CHECK(evaluateRPN("10 2 /") == 5);
}
TEST_CASE("exceptions"){
CHECK_THROWS(evaluateRPN("1 +"));
CHECK_THROWS(evaluateRPN("3 0 /"));
CHECK_THROWS(evaluateRPN("1 2 3"));
}
</code></pre>
"""
nbText: """
下载 **doctest** 单文件头库并执行测试:
"""
nbNewSection("第 3 章:交互式 REPL")
nbText: """
借助 `linenoise` 单文件头库实现行编辑与历史。
"""
nbSubSection("repl.cpp")
nbRawHtml: """
<pre><code class="language-cpp">#include "rpn_engine.hpp"
#include "linenoise.hpp"
#include <iomanip>
int main(){
const char* prompt = "rpn> ";
std::string line;
while (linenoise::Readline(prompt, line)){
if (line == "exit" || line == "quit") break;
try {
double v = evaluateRPN(line);
std::cout << "= " << std::setprecision(12) << v << '\n';
linenoise::AddHistory(line.c_str());
} catch (const std::exception& ex){
std::cerr << "error: " << ex.what() << '\n';
}
}
}
</code></pre>
"""
nbNewSection("第 4 章:单文件发布 & 交叉编译")
nbText: """
把全部源码合并成 `rpn_single.cpp`,`-Os -s -DNDEBUG` 优化后仅 ~92 KB(UPX 后)。
"""
nbSubSection("Linux→Windows 交叉编译")
nbNewSection("第 5 章:课后挑战")
nbText: """
1. 增加幂运算 (`^`) 与单目负号
2. 支持变量存储 (`sto` / `rcl`)
3. 用 `std::variant` 实现高精度有理数,避免浮点误差
4. 编译为 **WebAssembly**,在浏览器里运行!
祝 hacking 愉快!
"""
nbSave()
nbText: """
"""