news 2026/10/9 1:53:45

GoogleTest C++单元测试框架完全指南:从零集成到CI实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
GoogleTest C++单元测试框架完全指南:从零集成到CI实践

简介:GoogleTest是Google开源的C++单元测试框架,基于xUnit架构,面向需要系统开展代码测试的开发者、测试工程师及框架学习者。它支持自动测试发现、丰富的断言集、用户自定义断言与死亡测试,能够区分致命和非致命故障,并通过值参数化、类型参数化测试覆盖不同输入和数据类型,显著提升测试设计的灵活性和覆盖率。压缩包共248个文件,大小仅1.05MB,以cc源码和h头文件为主(合计155个),辅以Python脚本、Markdown文档及bazel/cmake构建配置,便于阅读框架实现、运行示例并集成到现有工程。已有245人参与学习下载。该包内含完整框架源码、多维示例与文档,可帮助读者深入理解xUnit设计思想,快速上手编写高复用参数化用例,并掌握自定义断言与测试运行选项,适合C++中高级开发者按需取用。

1. GoogleTest到底是什么:为什么 C++ 单元测试基本绕不开它

GoogleTest(谷歌 C++ 测试框架)是 C++ 社区里使用面最广的单元测试方案之一,它解决的核心问题很朴素:让你能用最小的成本把「函数行为是否符合预期」这件事变成可重复、可自动化的检查。很多人第一次接触它是在某个开源项目里看到TEST()宏,或者在 CMake 里看到enable_testing(),但真正把它用明白的人并不算多。

我见过不少开发者,项目里已经集成了 GoogleTest,写起测试来却还是在用if加printf手工验证,跑完看一眼终端输出就当作测试通过。这种做法的隐患在于:一旦代码进入重构期,行为回归不会有人提醒你,而 GoogleTest 的价值恰恰是把「验证」这件事从人工观察变成机器判定。这篇文章会从零开始讲透这个框架:怎么接入工程、断言怎么选、夹具和参数化怎么用、哪些地方最容易踩坑,以及如何把它接入 CI 和覆盖率统计。适合刚接触 C++ 测试的开发者,也适合已经写了几个测试文件但想系统补一遍边界知识的熟手。

2. 从零搭起一个可运行的测试工程:CMake 集成与三个最常用的断言族

2.1 最小工程长什么样:CMakeLists 与第一个冒烟测试

先不谈理论,直接看一个能编译、能跑、能失败的工程是什么样。假设你有一个简单的计算器模块,头文件calculator.h里声明了一个加法函数,你要为它写测试。最常见的组织方式是把测试代码放在tests/目录下,通过 CMake 的find_package或FetchContent把 GoogleTest 拉进来。

cmake_minimum_required(VERSION 3.14) project(CalcDemo CXX) enable_testing() # 用 FetchContent 自动拉取 GoogleTest 源码 include(FetchContent) FetchContent_Declare( googletest URL https://github.com/google/googletest/releases/download/v1.14.0/googletest-1.14.0.tar.gz ) FetchContent_MakeAvailable(googletest) add_executable(calc_tests test_calculator.cpp) target_link_libraries(calc_tests PRIVATE GTest::gtest_main) add_test(NAME calc_tests COMMAND calc_tests)

这个 CMakeLists 做了四件事:声明项目、开启测试支持、拉取并编译 GoogleTest、把测试可执行文件注册到 CTest。注意我链接的是GTest::gtest_main,它自带了一个main函数,你不用自己写;如果链接GTest::gtest,则需要手动定义一个调用RUN_ALL_TESTS()的main。

对应的测试文件长这样:

#include <gtest/gtest.h> #include "calculator.h" TEST(CalculatorTest, AddWorks) { EXPECT_EQ(add(2, 3), 5); EXPECT_EQ(add(-1, 1), 0); } TEST(CalculatorTest, AddHandlesZero) { EXPECT_EQ(add(0, 0), 0); EXPECT_EQ(add(0, 5), 5); }

这里TEST宏的第一个参数是测试套件名,第二个是测试名,两者合起来构成一个唯一的测试用例标识。编译后运行ctest或直接执行calc_tests,输出里会显示每个测试的通过状态。如果add函数实现有误,EXPECT_EQ会打印出期望值和实际值,测试以非零退出码结束,CTest 会把它标记为失败。

2.2 EXPECT_* 与 ASSERT_*:断言族的选型逻辑与参数细节

GoogleTest 的断言分两大类:EXPECT_*失败后继续执行当前测试,ASSERT_*失败后立即终止当前测试函数。这个区别在实际使用中非常关键。比如你要测试一个函数返回结构体,再用结构体里的多个字段做后续断言,如果第一次断言就失败,继续往下读字段很可能引发崩溃或产生误导性的二次报错,这时应该用ASSERT_*。

TEST(ParserTest, ParsesValidInput) { auto result = parse_line("name=alice;age=30"); ASSERT_TRUE(result.has_value()) << "解析失败,原始输入: name=alice;age=30"; ASSERT_EQ(result->age, 30); EXPECT_EQ(result->name, "alice"); }

参数化版本的断言还会接受一个自定义消息,通过<<追加输出,方便定位是哪一个输入触发了失败。我一般遵循两条原则:第一条,后续逻辑依赖当前结果时用ASSERT_*,否则用EXPECT_*;第二条,自定义消息里带上入参或上下文,否则排查问题时得重新加日志编译一次,非常浪费时间。

常见的断言族包括EXPECT_EQ / ASSERT_EQ(数值与字符串比较)、EXPECT_TRUE / ASSERT_TRUE(布尔条件)、EXPECT_FLOAT_EQ / ASSERT_FLOAT_EQ(浮点比较,带 4 个 ULP 容差)、EXPECT_NEAR(绝对误差比较)。浮点断言经常被忽略,直接拿EXPECT_EQ比两个浮点数,一旦涉及计算顺序变化就偶发失败,这是典型的玄学问题,实际原因就是精度。

2.3 测试夹具 TEST_F:共享数据与生命周期控制的正确姿势

当多个测试用例需要相同的准备数据和清理逻辑时,把重复代码写在每个TEST里会让维护成本迅速上升。GoogleTest 的测试夹具(Test Fixture)通过继承::testing::Test来解决这个问题:公共的初始化放在SetUp(),清理放在TearDown(),成员变量在用例之间各自独立。

class StackTest : public ::testing::Test { protected: void SetUp() override { for (int i = 0; i < 3; ++i) { stack_.push(i); } } void TearDown() override {} std::vector<int> stack_; }; TEST_F(StackTest, PopReturnsLastElement) { ASSERT_EQ(stack_.size(), 3u); EXPECT_EQ(stack_.back(), 2); stack_.pop_back(); EXPECT_EQ(stack_.size(), 2u); } TEST_F(StackTest, PushIncreasesSize) { stack_.push_back(42); EXPECT_EQ(stack_.size(), 4u); }

注意TEST_F的测试套件名必须和夹具类名完全一致,否则编译会报错。每个TEST_F在执行前都会重新构造一次夹具对象,并调用SetUp(),所以第二个测试看到的是一个全新的、只 push 了 0/1/2 的栈,而不是被第一个测试改过的栈。很多人第一次写TEST_F会在这里翻车,以为成员变量是跨用例共享的。

TearDown在多数场景下可以留空,因为成员对象的析构会自动执行;只有当你持有裸指针、文件句柄或需要显式释放的外部资源时,才必须写清理逻辑。

2.4 参数化测试:用 TEST_P 把一组输入跑成十组用例

参数化测试解决的是「同一份断言逻辑、多组输入数据」的场景。与其复制粘贴十个TEST,不如写一个模板化的测试体,让框架自动为每组参数生成一个独立的测试用例。

class ParamAddTest : public ::testing::TestWithParam<std::tuple<int, int, int>> { }; TEST_P(ParamAddTest, AddsCorrectly) { auto [a, b, expected] = GetParam(); EXPECT_EQ(add(a, b), expected); } INSTANTIATE_TEST_SUITE_P( AddCases, ParamAddTest, ::testing::Values( std::make_tuple(1, 2, 3), std::make_tuple(-1, 1, 0), std::make_tuple(100, 200, 300), std::make_tuple(0, 0, 0)));

TestWithParam<T>接受一个类型参数,GetParam()取出当前用例的参数值。INSTANTIATE_TEST_SUITE_P的三个参数依次是实例名前缀、测试套件名、参数生成器。这里用Values枚举具体的 tuple 值,也可以用Range生成连续的数值区间。

实例化后,每个参数组合会变成一个独立的测试用例,在终端输出里可以看到类似AddCases/ParamAddTest.AddsCorrectly/0这样的名字。参数化测试的价值不仅是省代码,更重要的是让测试报告能精确指出哪一组数据失败了。注意INSTANTIATE_TEST_SUITE_P必须写在全局作用域,不能放在函数内部或类内部,否则编译不过。

3. 测试结果与输出控制:从终端可读性到 XML/JSON 报告

3.1 失败信息的可读性:自定义消息与流式输出

默认情况下,EXPECT_EQ失败时会打印两行内容:期望值和实际值。但仅凭这两个数字,很多时候你根本想不起来是哪个业务场景出了问题。GoogleTest 支持两种补救手段:断言宏后接<<追加上下文,以及在作用域内用SCOPED_TRACE声明一个跟踪信息。

TEST(OrderTest, DiscountApplies) { const int user_level = 2; const double amount = 199.0; SCOPED_TRACE("user_level=" + std::to_string(user_level) + ", amount=" + std::to_string(amount)); double result = calculate_discount(user_level, amount); EXPECT_NEAR(result, 179.1, 0.01) << "折扣计算偏离预期"; }

SCOPED_TRACE的输出会出现在该作用域内所有断言失败的日志里,特别适合放在循环内部,因为循环里的断言失败时,你立刻就能知道是第几次迭代出的问题。<<追加的消息则更适合标注单一断言的业务含义。

你会发现,加不加这些消息,在测试通过时毫无区别;但一个月后某个用例挂了,报错信息里有没有上下文,直接决定了排查时间是五分钟还是半小时。我见过有人嫌麻烦不加消息,后来定位一个偶发失败花了整整一下午,最后发现是时区转换的边界问题——当时的教训是:所有涉及外部输入或复杂计算的断言,默认都加上下文消息,没有例外。

3.2 过滤与排序:命令行参数怎么筛出要跑的用例

大型工程里测试用例动辄上千个,全量跑一次可能耗时很久。开发期间你可能只想跑某几个相关的测试,或者某个测试套件下的全部用例。GoogleTest 的命令行参数直接支持这种筛选,不需要改代码、不需要注释。

// 运行所有名字匹配 *Parser* 的测试 ./calc_tests --gtest_filter=*Parser* // 运行指定套件下的指定用例 ./calc_tests --gtest_filter=ParserTest.ValidInput // 排除某些用例,使用负向匹配 ./calc_tests --gtest_filter=P*.*:-*Slow*

--gtest_filter支持*通配符和:分隔的多个模式,负向模式用-前缀。这比在代码里临时注释测试要安全得多——注释代码容易忘记恢复,而命令行参数只是影响本次运行。配合--gtest_list_tests可以先列出所有测试名,确认过滤规则写对了再执行。

另一个实用参数是--gtest_shuffle,它会随机打乱测试执行顺序。某些测试之间存在隐式的状态依赖(比如依赖静态变量的残留值),这种依赖在固定顺序下可能永远不被发现,而 shuffle 模式能把这类隐藏问题暴露出来。CI 里定期跑一次 shuffle 是个值得养成的习惯。

3.3 报告导出:XML/JSON 输出接入 CI 的常见做法

本地跑测试可以看终端输出,但接入流水线后,测试结果需要被机器解析。GoogleTest 原生支持两种结构化输出格式:XML 和 JSON。通过--gtest_output参数指定文件路径即可。

./calc_tests --gtest_output=xml:test_results.xml ./calc_tests --gtest_output=json:test_results.json

XML 格式兼容传统的 JUnit 风格,很多 CI 系统(比如常见的 Jenkins 或 GitLab CI)都原生支持解析这种格式,失败时会直接标注在流水线报告里。JSON 格式相对更简洁,适合自己写脚本处理。需要注意的是,输出路径可以包含目录,但目录必须预先存在,GoogleTest 不会自动创建目录,这一点经常导致 CI 上报告文件生成失败。

<testsuites tests="5" failures="1"> <testsuite name="CalculatorTest" tests="2" failures="0"> <testcase name="AddWorks" status="run"/> </testsuite> </testsuites>

如果你同时跑了多个测试可执行文件,每个文件都会生成一份独立的报告。常见做法是先合并再解析,或者在 CI 配置里按文件逐个上报。

4. 谷歌测试框架避坑指南:5 个让新手翻车的真实场景

4.1 现象:测试全绿,但代码明显有问题

某个开发者写了一个TEST,测试一个trim函数,代码如下:

TEST(UtilsTest, TrimRemovesSpaces) { std::string s = " hello "; trim(s); EXPECT_EQ(s, "hello"); }

运行结果是绿的。后来发现trim根本没实现,内部是空函数。原因在于:测试传入的字符串首尾是普通空格,而trim的空函数没有改动它,但EXPECT_EQ比较的是整个字符串,按道理应该失败。实际上这个开发者写的是EXPECT_STREQ的误用版本,或者断言写错了对象。

真正的坑不在断言本身,而在于:测试的断言强度不足。比如用EXPECT_TRUE(result.size() > 0)代替了精确比较,或者测试只覆盖了「不崩溃」而没有覆盖「行为正确」。解决方法是:每个断言都问自己一句,如果被测函数被替换成空实现,这个测试能发现吗?如果发现不了,说明断言太弱。测试的价值在于它能捕获回归,而不是证明代码能跑。

4.2 现象:夹具成员在 TEST_F 里是「脏」的

有开发者在SetUp()里初始化了一个成员std::map<int, std::string> table_,在TEST_F里向其中插入数据,然后写第二个TEST_F期望它是一个空表,结果第二个测试失败了,因为表里还有第一个测试插入的数据。

原因是对测试夹具生命周期的误解。每个TEST_F执行前,框架都会创建一个全新的夹具实例,并调用SetUp()。但如果你在SetUp()里没有完全重置所有成员(比如某些成员是局部静态变量、全局单例,或者SetUp()里只做了部分初始化),残留状态就会串到下一个用例。解决方法是:确保SetUp()覆盖所有成员的初始化;如果成员持有外部资源,在TearDown()里显式释放。更隐蔽的情况是静态局部变量——它跨所有用例存活,需要在每个用例里显式重置。

4.3 现象:参数化测试的名字里出现乱码

INSTANTIATE_TEST_SUITE_P实例化后,GoogleTest 会用参数的打印值来生成测试名后缀。如果参数类型没有实现operator<<,或者打印结果是空字符串,测试名会变成难以阅读的十六进制地址或乱码。更严重的是,如果两组参数的打印值完全相同,测试名会冲突,导致部分用例被跳过。

解决方法是:为自定义类型实现operator<<,或者用::testing::PrintToString定制打印。如果只是想要有意义的用例名,可以在INSTANTIATE_TEST_SUITE_P的第四个参数传入一个命名函数。常见做法是为参数生成器配上::testing::Values("case1", "case2")这种自带可读性的枚举类型,避免直接用裸指针或复杂结构体做参数。

4.4 现象:ASSERT_* 在子函数里失效

有开发者把断言封装到一个辅助函数里:

void check_result(const Result& r) { ASSERT_TRUE(r.ok); } TEST(ServiceTest, HandleRequest) { auto r = process_request(); check_result(r); // 这里的代码在 r.ok 为 false 时依然会执行 }

原因是ASSERT_*失败时执行的是return,但它是从当前函数返回,而不是从调用它的TEST函数返回。因此check_result里的ASSERT_TRUE失败后,只是结束check_result,后续代码照常运行。这是 GoogleTest 最经典的作用域陷阱。

解决方法是:把子函数设计成返回布尔值,在测试函数里用ASSERT_TRUE(check_result(r));或者把断言直接写在测试函数里;如果必须在子函数里终止整个测试,可以让子函数返回::testing::AssertionResult,再配合ASSERT_TRUE使用。这种模式还能顺便输出详细的失败信息,比裸的ASSERT_TRUE更利于排查。

4.5 现象:CMake 链接顺序导致 undefined reference

在 Linux 下用 g++ 直接编译时,静态库的链接顺序是从右往左解析的。如果你把gtest_main写在其他库的左边,某些版本的链接器会报undefined reference to testing::Test::SetUp()之类的错误。这个坑在 CMake 的target_link_libraries里不一定出现,因为 CMake 会帮你处理传递依赖;但如果用了add_test加自定义命令,或者手动写 Makefile,顺序问题就非常突出。

target_link_libraries(calc_tests PRIVATE GTest::gtest_main my_module)

GTest::gtest_main是 CMake 导入的目标,它会自动传递GTest::gtest和GTest::gmock的链接关系。如果你看到链接错误,先检查是否链接了GTest::gtest_main;如果链接了gtest但没链pthread,也可能出问题。另一个常见原因是 GoogleTest 版本较旧,CMake 里没有提供 imported target,需要手动指定gtest_main和pthread,并把 include 目录指对。

5. 与主流工具链配合:覆盖率、监听器与 CI 里的实际用法

5.1 收集代码覆盖率:gcov/lcov 与 GoogleTest 的配合

GoogleTest 本身不收集覆盖率,但它生成的测试程序天然适合配合 gcov/lcov 这类工具。覆盖率的价值在于发现「没被测试到的分支」,而不是追求一个好看的数字。我见过有人为了把覆盖率从 80% 凑到 95% 而写一堆只调用不验证的测试,这种测试除了让报告好看,对质量毫无贡献。

target_compile_options(calc_tests PRIVATE --coverage) target_link_options(calc_tests PRIVATE --coverage)
# 编译并运行测试 cmake --build build -j cd build ./calc_tests # 生成覆盖率报告 lcov --capture --directory . --output-file coverage.info lcov --remove coverage.info '*/tests/*' '/usr/*' --output-file coverage_filtered.info genhtml coverage_filtered.info --output-directory coverage_html

--coverage同时开启了编译插桩和链接时的 gcov 运行时。测试跑完后,目录下会出现多个.gcda文件,lcov --capture会汇总这些数据。注意lcov --remove这一步非常重要:如果不排除 GoogleTest 自身的源码和你不想统计的目录,报告里的覆盖率会被严重稀释。浏览器打开coverage_html/index.html可以看到每个函数的命中情况。

5.2 自定义事件监听器:在测试运行时拿到第一手进度

默认的终端输出已经够用,但接入 CI 或开发一个测试管理工具时,你可能需要实时感知测试的开始、结束和失败信息。GoogleTest 提供了TestEventListener接口,可以自定义事件回调。

#include <gtest/gtest.h> class ProgressListener : public ::testing::EmptyTestEventListener { public: void OnTestStart(const ::testing::TestInfo& info) override { std::cerr << "[RUN] " << info.test_suite_name() << "." << info.name() << std::endl; } void OnTestEnd(const ::testing::TestInfo& info) override { if (info.result()->Failed()) { std::cerr << "[FAILED] " << info.test_suite_name() << "." << info.name() << std::endl; } } }; int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); ::testing::UnitTest::GetInstance()->listeners().Append(new ProgressListener); return RUN_ALL_TESTS(); }

这里你需要链接GTest::gtest(不带_main),自己写main函数。EmptyTestEventListener提供了一组空实现的回调,你只需要覆写关心的那几个。注意Append是追加监听器,默认的打印监听器依然生效;如果不想看到默认输出,可以用Release把它移除。监听器在跨平台项目里还要注意线程安全:如果你用了gtest的并发测试功能,回调可能从多个线程触发,自定义监听器里不要操作非线程安全的对象。

5.3 在 CI 里跑测试:退出码、超时与失败重试

CI 里运行 GoogleTest 测试,一定要理解退出码的意义。RUN_ALL_TESTS()返回的退出码非零即表示至少一个测试失败,这也是 CTest 和大多数 CI 系统判断成功与否的依据。但有几个细节容易被忽略。

# 超时控制:单个测试卡死会拖垮整个流水线 timeout 120 ./calc_tests # 失败时输出更多上下文 ./calc_tests --gtest_break_on_failure

线程死锁或意外阻塞的测试会让流水线一直挂到全局超时。常见的做法是给测试程序套上timeout限制,或者在 CI 配置里设置 job 超时。--gtest_break_on_failure会在失败时触发调试器断点,本地排查时非常有用,CI 上一般不用它。

关于失败重试,我不建议在 CI 里自动重跑全量测试。偶发失败的正确处理方式是先拿日志定位原因——大多数「偶发」实际上是测试之间共享了静态状态、用到了固定端口或依赖了系统时间。直接重试会掩盖这些真实问题,让它们变成长期悬案。如果确实有不可避免的时序依赖(比如测试外部服务的网络抖动),应当用--gtest_repeat加上固定次数在本地复现,而不是在流水线里偷偷重试。

6. 进阶技巧:测试私有成员、死亡测试与 mock 的取舍

class MyClass { private: int internal_state_; int compute_impl(int x); FRIEND_TEST(PrivateTest, AccessInternalState); }; TEST(PrivateTest, AccessInternalState) { MyClass obj; EXPECT_EQ(obj.compute_impl(2), 10); }

当被测类的私有方法承载核心逻辑,而公开接口只做参数校验时,只测公开接口往往覆盖不到关键分支。GoogleTest 的FRIEND_TEST宏能让测试类访问被测类的私有成员,前提是测试套件名要与宏声明一致。这个方案比#define private public干净得多——后者是全局生效,会让所有私有成员变成公开,且影响整个编译单元,万一忘记 undef 会污染后续代码。FRIEND_TEST是对 C++ 友元机制的合理使用,局限性是需要修改被测类的声明,不适合第三方库。

死亡测试是另一个常被忽略的功能。对于assert、abort或exit这类「预期崩溃」的行为,普通断言无法验证,需要用EXPECT_DEATH包裹。

TEST(DeathTest, AssertionTriggers) { EXPECT_DEATH(trigger_assert(0), "index out of range"); }

注意死亡测试在TEST名的死亡承诺上是反直觉的:测试本身会启动子进程执行被测代码,父进程捕获子进程的退出状态。因此死亡测试的开销远高于普通测试,一个进程 fork 可能需要几毫秒到几十毫秒,大规模使用时会影响整体耗时。而且在某些平台(比如 Windows)上,死亡测试的实现依赖fork或_spawn,兼容性和稳定性不如 POSIX 环境,涉及跨平台时要预留额外的排障时间。

关于 mock 的取舍,我的建议是:优先写真实依赖的集成测试,把 mock 用在真正的边界上。纯单元测试里 mock 掉所有外部组件,测试是快了,但测的全是你自己的假设,而不是真实行为。GoogleTest 自带的 GoogleMock 适合 mock 接口类(比如网络客户端、数据库访问层),用法上它的MOCK_METHOD宏、EXPECT_CALL和ON_CALL组合能写出非常精确的交互验证,但学习曲线比 GoogleTest 本体陡得多。如果你的项目已经集成了 GoogleTest,引入 GoogleMock 通常不需要额外安装依赖,直接在同一个链接目标里加GTest::gmock即可。

说实话,用了这么多年 GoogleTest,我最大的教训是:测试框架本身不难,难的是对被测代码行为的准确建模。与其花时间研究花哨的宏和语法,不如在写测试前想清楚三个问题——这个函数正确性的边界是什么、哪些错误不能被吞掉、重构时哪些行为必须保持不变。养成这个习惯以后,大部分测试陷阱都可以绕开。希望这篇笔记能帮你少走一些弯路。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/9 1:51:17

别再让 AI 猜你的需求:AIM + MAP 双框架,把提示词一次写准

很多人和 AI 的对话体验其实差不多&#xff1a;问了不少&#xff0c;答案不少&#xff0c;真正能用的没几个。问题通常不在 AI 的能力&#xff0c;而在提问本身 —— 身份没说清、背景没给全、要求想到哪说到哪。结果就是那句老话&#xff1a;想要的答案得不到&#xff0c;得到…

作者头像 李华
网站建设 2026/10/9 1:50:24

openrig 实战:Node.js + tmux 搭建 Claude Code 与 Codex 稳定开发环境

1. 从"openrig"这个名字说起&#xff1a;它到底想解决什么问题第一次看到openrig这个词&#xff0c;我脑子里蹦出来的画面是矿场里的钻井平台——rig 在英文里本来就有"装备、装置、平台"的意思&#xff0c;open 则代表开放。把这两个词拼在一起&#xff0…

作者头像 李华
网站建设 2026/10/9 1:47:46

LeetCode 1047 题解:删除字符串中的所有相邻重复项——栈与数组模拟的多种实现(LogicStack-LeetCode 刷题笔记)

教程文档 【免费下载链接】LogicStack-LeetCode 公众号「宫水三叶的刷题日记」刷穿 LeetCode 系列文章源码 项目地址&#xff1a; https://gitcode.com/gh_mirrors/lo/LogicStack-LeetCode 点击查看 免费下载 导读 本文围绕 LeetCode 第 1047 题「删除字符串中的所有相邻重复…

作者头像 李华