面向 C++ 的现代 CMake 教程(三)(5)

简介: 面向 C++ 的现代 CMake 教程(三)

面向 C++ 的现代 CMake 教程(三)(4)https://developer.aliyun.com/article/1525579

GMock

编写真正的单元测试是关于从其他代码中隔离执行一段代码。这样的单元被理解为一个自包含的元素,要么是一个类,要么是一个组件。当然,用 C++编写的几乎没有任何程序将它们的所有单元与其他单元清晰地隔离。很可能,你的代码将严重依赖于类之间某种形式的关联关系。这种关系有一个问题:此类对象将需要另一个类的对象,而这些将需要另一个。在不知不觉中,你的整个解决方案就参与了一个“单元测试”。更糟糕的是,你的代码可能与外部系统耦合,并依赖于其状态——例如,数据库中的特定记录,网络数据包的传入,或磁盘上存储的特定文件。

为了测试目的而解耦单元,开发人员使用测试替身或类的特殊版本,这些类被测试类使用。一些例子包括伪造品、存根和模拟。以下是这些的一些大致定义:

  • 伪造品是某些更复杂类的有限实现。一个例子可能是在实际数据库客户端之内的内存映射。
  • 存根为方法调用提供特定的、预先录制的回答,限于测试中使用的回答。它还可以记录调用了哪些方法以及发生了多少次。
  • 模拟是存根的一个更扩展版本。它还将验证测试期间方法是否如预期地被调用。

这样一个测试替身是在测试开始时创建的,作为测试类构造函数的参数提供,以代替真实对象使用。这种机制称为依赖注入

简单测试替身的问题是它们太简单。为了为不同的测试场景模拟行为,我们可能需要提供许多不同的替身,每一个都是耦合对象可能处于的不同状态。这并不实用,并且会将测试代码分散到太多的文件中。这就是 GMock 出现的地方:它允许开发人员为特定类创建一个通用的测试替身,并在每一行中定义其行为。GMock 将这些替身称为“模拟”,但实际上,它们是上述所有类型的混合,具体取决于场合。

考虑以下示例:让我们为我们的Calc类添加一个功能,它将提供一个随机数添加到提供的参数。它将通过一个AddRandomNumber()方法表示,该方法返回这个和作为一个int。我们如何确认返回的值确实是随机数和提供给类的值的准确和?正如我们所知,依赖随机性是许多重要过程的关键,如果我们使用不当,我们可能会遭受各种后果。检查所有随机数直到我们耗尽所有可能性并不太实用。

为了测试它,我们需要将一个随机数生成器封装在一个可以被模拟(或者说,用一个模拟对象替换)的类中。模拟对象将允许我们强制一个特定的响应,即“伪造”一个随机数的生成。Calc将在AddRandomNumber()中使用这个值,并允许我们检查该方法返回的值是否符合预期。将随机数生成分离到另一个单元中是一个额外的价值(因为我们将能够交换一种生成器类型为另一种)。

让我们从抽象生成器的公共接口开始。这将允许我们在实际生成器和模拟中实现它,使其可以相互替换。我们将执行以下代码:

chapter08/05-gmock/src/rng.h

面向 C++ 的现代 CMake 教程(三)

实现此接口的类将从Get()方法提供随机数。注意virtual关键字——除非我们希望涉及更复杂的基于模板的模拟,否则所有要模拟的方法都必须有它,除非我们希望涉及更复杂的基于模板的模拟。我们还需要记得添加一个虚拟析构函数。接下来,我们需要扩展我们的Calc类以接受和存储生成器,如下所示:

第八章/05-gmock/源码/calc.h

#pragma once
#include "rng.h"
class Calc {
  RandomNumberGenerator* rng_;
 public:
   Calc(RandomNumberGenerator* rng);
   int Sum(int a, int b);
   int Multiply(int a, int b);
   int AddRandomNumber(int a);
};

我们包含了头文件并添加了一个提供随机增加的方法。此外,创建了一个存储生成器指针的字段以及一个参数化构造函数。这就是依赖注入在实际工作中的运作方式。现在,我们实现这些方法,如下所示:

第八章/05-gmock/源码/calc.cpp

#include "calc.h"
Calc::Calc(RandomNumberGenerator* rng) {
  rng_ = rng;
}
int Calc::Sum(int a, int b) {
  return a + b;
}
int Calc::Multiply(int a, int b) {
  return a * b; // now corrected
}
int Calc::AddRandomNumber(int a) {
  return a + rng_->Get();
}

在构造函数中,我们将提供的指针赋值给一个类字段。然后我们在AddRandomNumber()中使用这个字段来获取生成的值。生产代码将使用一个真正的数字生成器;测试将使用模拟。记住我们需要对指针进行解引用以启用多态。作为奖励,我们可能为不同的实现创建不同的生成器类。我只需要一个:一个梅森旋转伪随机生成器,具有均匀分布,如下面的代码片段所示:

第八章/05-gmock/源码/rng_mt19937.cpp

#include <random>
#include "rng_mt19937.h"
int RandomNumberGeneratorMt19937::Get() {
  std::random_device rd;
  std::mt19937 gen(rd());
  std::uniform_int_distribution<> distrib(1, 6);
  return distrib(gen);
}

这段代码不是非常高效,但它将适用于这个简单的例子。目的是生成16之间的数字并将它们返回给调用者。这个类的头文件尽可能简单,正如我们所见:

第八章/05-gmock/源码/rng_mt19937.h

#include "rng.h"
class RandomNumberGeneratorMt19937
      : public RandomNumberGenerator {
 public:
  int Get() override;
};

这是我们如何在生产代码中使用它:

第八章/05-gmock/源码/运行.cpp

#include <iostream>
#include "calc.h"
#include "rng_mt19937.h"
using namespace std;
int run() {
  auto rng = new RandomNumberGeneratorMt19937();
  Calc c(rng);
  cout << "Random dice throw + 1 = " 
       << c.AddRandomNumber(1) << endl;
  delete rng;
  return 0; 
}

我们创建了一个生成器,并将它的指针传递给Calc的构造函数。一切准备就绪,我们可以开始编写我们的模拟。为了保持组织性,开发人员通常将模拟放在一个单独的test/mocks目录中。为了防止模糊性,头文件名有一个_mock后缀。我们将执行以下代码:

第八章/05-gmock/测试/模拟/rng_mock.h

#pragma once
#include "gmock/gmock.h"
class RandomNumberGeneratorMock : public
 RandomNumberGenerator {
 public:
  MOCK_METHOD(int, Get, (), (override));
};

在添加gmock.h头文件后,我们可以声明我们的模拟。如计划,它是一个实现RandomNumberGenerator接口的类。我们不需要自己编写方法,需要使用 GMock 提供的MOCK_METHOD宏。这些通知框架应该模拟接口中的哪些方法。使用以下格式(注意括号):

MOCK_METHOD(<return type>, <method name>, 
           (<argument list>), (<keywords>))

我们准备好在我们的测试套件中使用模拟(为了简洁,省略了之前的测试案例),如下所示:

第八章/05-gmock/测试/calc_test.cpp

#include <gtest/gtest.h>
#include "calc.h"
#include "mocks/rng_mock.h"
using namespace ::testing;
class CalcTestSuite : public Test {
 protected:
  RandomNumberGeneratorMock rng_mock_;
  Calc sut_{&rng_mock_};
};
TEST_F(CalcTestSuite, AddRandomNumberAddsThree) {
  EXPECT_CALL(rng_mock_,
Get()).Times(1).WillOnce(Return(3));
  EXPECT_EQ(4, sut_.AddRandomNumber(1));
}

让我们分解一下更改:我们在测试套件中添加了新的头文件并为rng_mock_创建了一个新字段。接下来,将模拟的地址传递给sut_的构造函数。我们可以这样做,因为字段是按声明顺序初始化的(rng_mock_先于sut_)。

在我们的测试用例中,我们对rng_mock_Get()方法调用 GMock 的EXPECT_CALL宏。这告诉框架,如果在执行过程中没有调用Get()方法,则测试失败。Times链式调用明确指出,为了测试通过,必须发生多少次调用。WillOnce确定在方法调用后,模拟框架做什么(它返回3)。

借助 GMock,我们能够一边表达期望的结果,一边表达被模拟的行为。这极大地提高了可读性,并使得测试的维护变得更加容易。最重要的是,它在每个测试用例中提供了弹性,因为我们可以通过一个单一的表达式来区分发生了什么。

最后,我们需要确保gmock库与一个测试运行器链接。为了实现这一点,我们需要将其添加到target_link_libraries()列表中,如下所示:

第八章/05-gmock/test/CMakeLists.txt

include(FetchContent)
FetchContent_Declare(
  googletest
  GIT_REPOSITORY https://github.com/google/googletest.git
  GIT_TAG release-1.11.0
)
# For Windows: Prevent overriding the parent project's
  compiler/linker settings
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(googletest)
add_executable(unit_tests
               calc_test.cpp
               run_test.cpp)
target_link_libraries(unit_tests PRIVATE sut gtest_main
gmock)
include(GoogleTest)
gtest_discover_tests(unit_tests)

现在,我们可以享受 GTest 框架的所有好处。GTest 和 GMock 都是非常先进的工具,拥有大量的概念、实用程序和帮助器,适用于不同的场合。这个例子(尽管有点长)只是触及了可能实现的功能的表面。我鼓励你将它们纳入你的项目中,因为它们将极大地提高你的代码质量。开始使用 GMock 的一个好地方是官方文档中的Mocking for Dummies页面(你可以在进阶阅读部分找到这个链接)。

有了测试之后,我们应该以某种方式衡量哪些部分被测试了,哪些没有,并努力改善这种情况。最好使用自动化工具来收集和报告这些信息。

生成测试覆盖报告

向如此小的解决方案中添加测试并不是非常具有挑战性。真正的困难来自于稍微高级一些和更长的程序。多年来,我发现当我接近 1,000 行代码时,逐渐变得难以跟踪测试中执行了哪些行和分支,哪些没有。超过 3,000 行后,几乎是不可能的。大多数专业应用程序将拥有比这更多的代码。为了解决这个问题,我们可以使用一个工具来了解哪些代码行被“测试用例覆盖”。这样的代码覆盖工具连接到 SUT,并在测试中收集每行的执行信息,以方便的报告形式呈现,就像这里显示的这样:

图 8.3 ‒ 由 LCOV 生成的代码覆盖报告

这些报告将显示哪些文件被测试覆盖了,哪些没有。更重要的是,你还可以查看每个文件的具体细节,确切地看到哪些代码行被执行了,以及这种情况发生了多少次。在下面的屏幕截图中,Calc 构造函数被执行了 4 次,每次都是针对不同的测试:

图 8.4 ‒ 代码覆盖报告的详细视图

生成类似报告有多种方法,它们在平台和编译器之间有所不同,但它们通常遵循相同的程序:准备要测量的 SUT,获取基线,测量和报告。

执行这项工作的最简单工具名叫gcov,它是gcov的一个覆盖率工具,用于测量覆盖率。如果你在使用 Clang,不用担心——Clang 支持生成这种格式的指标。你可以从由Linux 测试项目维护的官方仓库获取 LCOV(github.com/linux-test-project/lcov),或者简单地使用包管理器。正如其名,它是一个面向 Linux 的工具。虽然可以在 macOS 上运行它,但不支持 Windows 平台。最终用户通常不关心测试覆盖率,所以通常可以手动在自建的构建环境中安装 LCOV,而不是将其绑定到项目中。

为了测量覆盖率,我们需要做以下工作:

  1. Debug配置编译,使用编译器标志启用代码覆盖。这将生成覆盖注释(.gcno)文件。
  2. 将测试可执行文件与gcov库链接。
  3. 在不运行任何测试的情况下收集基线覆盖率指标。
  4. 运行测试。这将创建覆盖数据(.gcda)文件。
  5. 将指标收集到聚合信息文件中。
  6. 生成一个(.html)报告。

我们应该首先解释为什么代码必须以Debug配置编译。最重要的原因是,通常Debug配置使用-O0标志禁用了任何优化。CMake 通过默认在CMAKE_CXX_FLAGS_DEBUG变量中实现这一点(尽管在文档中没有提到这一点)。除非你决定覆盖这个变量,否则你的调试构建应该是未优化的。这是为了防止任何内联和其他类型的隐式代码简化。否则,将很难追踪哪一行机器指令来自哪一行源代码。

在第一步中,我们需要指示编译器为我们的 SUT 添加必要的 instrumentation。需要添加的确切标志是编译器特定的;然而,两个主要的编译器—GCC 和 Clang—提供相同的--coverage标志,以启用覆盖率,生成 GCC 兼容的gcov格式的数据。

这就是我们如何将覆盖率 instrumentation 添加到前面章节中的示例 SUT:

chapter08/06-coverage/src/CMakeLists.txt

add_library(sut STATIC calc.cpp run.cpp rng_mt19937.cpp)
target_include_directories(sut PUBLIC .)
if (CMAKE_BUILD_TYPE STREQUAL Debug)
  target_compile_options(sut PRIVATE --coverage)
  target_link_options(sut PUBLIC --coverage)
  add_custom_command(TARGET sut PRE_BUILD COMMAND
                     find ${CMAKE_BINARY_DIR} -type f
                     -name '*.gcda' -exec rm {} +)
endif()
add_executable(bootstrap bootstrap.cpp)
target_link_libraries(bootstrap PRIVATE sut)
• 11

让我们逐步分解,如下所述:

  1. 确保我们正在使用if(STREQUAL)命令以Debug配置运行。记住,除非你使用-DCMAKE_BUILD_TYPE=Debug选项运行cmake,否则你无法获得任何覆盖率。
  2. sut库的所有object filesPRIVATE编译选项添加--coverage
  3. PUBLIC链接器选项添加--coverage: both GCC 和 Clang 将此解释为请求与所有依赖于sut的目标链接gcov(或兼容)库(由于传播属性)。
  4. add_custom_command()命令被引入以清除任何陈旧的.gcda文件。讨论添加此命令的原因在避免 SEGFAULT 陷阱部分中有详细说明。

这足以生成代码覆盖率。如果你使用的是 Clion 之类的 IDE,你将能够运行带有覆盖率的单元测试,并在内置的报告视图中获取结果。然而,这不会在 CI/CD 中运行的任何自动化管道中工作。要获取报告,我们需要自己使用 LCOV 生成它们。

为此目的,最好定义一个名为coverage的新目标。为了保持整洁,我们将在另一个文件中定义一个单独的函数AddCoverage,用于在test列表文件中使用,如下所示:

chapter08/06-coverage/cmake/Coverage.cmake

function(AddCoverage target)
  find_program(LCOV_PATH lcov REQUIRED)
  find_program(GENHTML_PATH genhtml REQUIRED)
  add_custom_target(coverage
    COMMENT "Running coverage for ${target}..."
    COMMAND ${LCOV_PATH} -d . --zerocounters
    COMMAND $<TARGET_FILE:${target}>
    COMMAND ${LCOV_PATH} -d . --capture -o coverage.info
    COMMAND ${LCOV_PATH} -r coverage.info '/usr/include/*' 
                         -o filtered.info
    COMMAND ${GENHTML_PATH} -o coverage filtered.info 
      --legend
    COMMAND rm -rf coverage.info filtered.info
    WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
  )
endfunction()

在前面的片段中,我们首先检测lcovgenhtml(来自 LCOV 包的两个命令行工具)的路径。REQUIRED关键字指示 CMake 在找不到它们时抛出错误。接下来,我们按照以下步骤添加一个自定义的coverage目标:

  1. 清除之前运行的任何计数器。
  2. 运行target可执行文件(使用生成器表达式获取其路径)。$是一个特殊的生成器表达式,在此情况下它会隐式地添加对target的依赖,使其在执行所有命令之前构建。我们将target作为此函数的参数提供。
  3. 从当前目录(-d .)收集解决方案的度量,并输出到文件(-o coverage.info)中。
  4. 删除(-r)不需要的覆盖数据('/usr/include/*')并输出到另一个文件(-o filtered.info)。
  5. coverage目录中生成 HTML 报告,并添加一个--legend颜色。
  6. 删除临时.info文件。
  7. 指定WORKING_DIRECTORY关键字可以将二叉树作为所有命令的工作目录。

这些是 GCC 和 Clang 通用的步骤,但重要的是要知道gcov工具的版本必须与编译器的版本匹配。换句话说,不能用 GCC 的gcov工具来编译 Clang 代码。要使lcov指向 Clang 的gcov工具,我们可以使用--gcov-tool参数。这里唯一的问题是它必须是一个单一的可执行文件。为了解决这个问题,我们可以提供一个简单的包装脚本(别忘了用chmod +x将其标记为可执行文件),如下所示:

cmake/gcov-llvm-wrapper.sh

#!/bin/bash
exec llvm-cov gcov "$@"

我们之前函数中所有对${LCOV_PATH}的调用应接受以下额外标志:

--gcov-tool ${CMAKE_SOURCE_DIR}/cmake/gcov-llvm-wrapper.sh

确保此函数可用于包含在test列表文件中。我们可以通过在主列表文件中扩展包含搜索路径来实现:

chapter08/06-coverage/CMakeLists.txt

cmake_minimum_required(VERSION 3.20.0)
project(Coverage CXX)
enable_testing()
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
add_subdirectory(src bin)
add_subdirectory(test)

这行小代码允许我们将cmake目录中的所有.cmake文件包括在我们的项目中。现在我们可以在test列表文件中使用Coverage.cmake,如下所示:

chapter08/06-coverage/test/CMakeLists.txt(片段)

# ... skipped unit_tests target declaration for brevity
include(Coverage)
AddCoverage(unit_tests)
include(GoogleTest)
gtest_discover_tests(unit_tests)

为了构建这个目标,请使用以下命令(注意第一个命令以DCMAKE_BUILD_TYPE=Debug构建类型选择结束):

# cmake -B <binary_tree> -S <source_tree> 
  -DCMAKE_BUILD_TYPE=Debug
# cmake --build <binary_tree> -t coverage

完成所有提到的步骤后,你将看到一个简短的摘要,就像这样:

Writing directory view page.
Overall coverage rate:
  lines......: 95.2% (20 of 21 lines)
  functions..: 75.0% (6 of 8 functions)
[100%] Built target coverage

接下来,在你的浏览器中打开coverage/index.html文件,享受这些报告吧!不过有一个小问题……

避免 SEGFAULT 陷阱

当我们开始在如此解决方案中编辑源代码时,我们可能会陷入困境。这是因为覆盖信息被分成两部分,如下所示:

  • gcno文件,或GNU 覆盖笔记,在 SUT 编译期间生成
  • gcda文件,或GNU 覆盖数据,在测试运行期间生成和更新

“更新”功能是段错误的一个潜在来源。在我们最初运行测试后,我们留下了许多gcda文件,在任何时候都没有被移除。如果我们对源代码做一些更改并重新编译对象文件,将创建新的gcno文件。然而,没有擦除步骤——旧的gcda文件仍然跟随过时的源代码。当我们执行unit_tests二进制文件(它在gtest_discover_tests宏中发生)时,覆盖信息文件将不匹配,我们将收到一个SEGFAULT(段错误)错误。

为了避免这个问题,我们应该删除任何过时的gcda文件。由于我们的sut实例是一个静态库,我们可以将add_custom_command(TARGET)命令挂钩到构建事件上。在重建开始之前,将执行清理。

进一步阅读部分找到更多信息链接。

摘要

在表面上看,似乎与适当测试相关的复杂性如此之大,以至于不值得付出努力。令人惊讶的是,运行没有任何测试的代码量有多少,主要论点是测试软件是一个令人畏惧的任务。我要补充的是:如果手动完成,更是如此。不幸的是,如果没有严格的自动化测试,代码中任何问题的可见性是不完整或根本不存在的。未测试的代码通常写起来更快(并非总是如此),但肯定更慢阅读、重构和修复。

在本章中,我们概述了从一开始就进行测试的一些关键原因。其中最引人入胜的是心理健康和一个良好的夜晚睡眠。没有开发人员会躺在床上想:“我迫不及待地想在几小时后醒来灭火和修复 bug。”但认真地说:在部署到生产之前捕获错误,可能对你(和公司)来说是个救命稻草。

谈到测试工具,CMake 确实显示了其真正的实力。CTest 可以在检测错误测试方面做到 wonders:隔离、洗牌、重复、超时。所有这些技术都非常方便,并且可以通过简单的命令行标志直接使用。我们还学会了如何使用 CTest 列出测试、过滤测试以及控制测试用例的输出,但最重要的是,我们现在知道了采用标准解决方案的真正力量。任何使用 CMake 构建的项目都可以以完全相同的方式进行测试,而无需调查其内部细节。

接下来,我们优化了项目结构,以简化测试过程并在生产代码和测试运行器之间复用相同的对象文件。编写自己的测试运行器很有趣,但也许让我们专注于程序应该解决的实际问题,并投入时间去拥抱一个流行的第三方测试框架。

说到这个,我们学习了 Catch2 和 GTest 的基础知识。我们进一步深入研究了 GMock 库的细节,并理解了测试替身是如何工作以使真正的单元测试成为可能的。最后,我们使用 LCOV 设置了报告。毕竟,没有什么比硬数据更能证明我们的解决方案实际上是完全测试过的了。

在下一章中,我们将讨论更多有用的工具来提高源代码的质量并发现我们甚至不知道存在的 issue。

进一步阅读

您可以通过以下链接获取更多信息:

github.com/catchorg/Catch2/blob/devel/docs/cmake-integration.md

github.com/catchorg/Catch2/blob/devel/docs/tutorial.md

gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html

gcc.gnu.org/onlinedocs/gcc/Invoking-Gcov.html

gcc.gnu.org/onlinedocs/gcc/Gcov-Data-Files.html

ltp.sourceforge.net/coverage/lcov/lcov.1.php

ltp.sourceforge.net/coverage/lcov/genhtml.1.php

进一步阅读部分找到更多信息链接。

摘要

在表面上看,似乎与适当测试相关的复杂性如此之大,以至于不值得付出努力。令人惊讶的是,运行没有任何测试的代码量有多少,主要论点是测试软件是一个令人畏惧的任务。我要补充的是:如果手动完成,更是如此。不幸的是,如果没有严格的自动化测试,代码中任何问题的可见性是不完整或根本不存在的。未测试的代码通常写起来更快(并非总是如此),但肯定更慢阅读、重构和修复。

在本章中,我们概述了从一开始就进行测试的一些关键原因。其中最引人入胜的是心理健康和一个良好的夜晚睡眠。没有开发人员会躺在床上想:“我迫不及待地想在几小时后醒来灭火和修复 bug。”但认真地说:在部署到生产之前捕获错误,可能对你(和公司)来说是个救命稻草。

谈到测试工具,CMake 确实显示了其真正的实力。CTest 可以在检测错误测试方面做到 wonders:隔离、洗牌、重复、超时。所有这些技术都非常方便,并且可以通过简单的命令行标志直接使用。我们还学会了如何使用 CTest 列出测试、过滤测试以及控制测试用例的输出,但最重要的是,我们现在知道了采用标准解决方案的真正力量。任何使用 CMake 构建的项目都可以以完全相同的方式进行测试,而无需调查其内部细节。

接下来,我们优化了项目结构,以简化测试过程并在生产代码和测试运行器之间复用相同的对象文件。编写自己的测试运行器很有趣,但也许让我们专注于程序应该解决的实际问题,并投入时间去拥抱一个流行的第三方测试框架。

说到这个,我们学习了 Catch2 和 GTest 的基础知识。我们进一步深入研究了 GMock 库的细节,并理解了测试替身是如何工作以使真正的单元测试成为可能的。最后,我们使用 LCOV 设置了报告。毕竟,没有什么比硬数据更能证明我们的解决方案实际上是完全测试过的了。

在下一章中,我们将讨论更多有用的工具来提高源代码的质量并发现我们甚至不知道存在的 issue。

进一步阅读

您可以通过以下链接获取更多信息:

github.com/catchorg/Catch2/blob/devel/docs/cmake-integration.md

github.com/catchorg/Catch2/blob/devel/docs/tutorial.md

gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html

gcc.gnu.org/onlinedocs/gcc/Invoking-Gcov.html

gcc.gnu.org/onlinedocs/gcc/Gcov-Data-Files.html

ltp.sourceforge.net/coverage/lcov/lcov.1.php

ltp.sourceforge.net/coverage/lcov/genhtml.1.php

相关文章
|
5天前
|
C++
Clion CMake C/C++程序输出乱码
Clion CMake C/C++程序输出乱码
7 0
|
6天前
|
存储 算法 编译器
C++ 函数式编程教程
C++ 函数式编程学习
|
6天前
|
存储 编译器 开发工具
C++语言教程分享
C++语言教程分享
|
6天前
|
存储 编译器 C++
|
27天前
|
C++ 存储 索引
面向 C++ 的现代 CMake 教程(一)(5)
面向 C++ 的现代 CMake 教程(一)
45 0
|
27天前
|
缓存 存储 C++
面向 C++ 的现代 CMake 教程(一)(4)
面向 C++ 的现代 CMake 教程(一)
45 0
|
27天前
|
C++ 缓存 存储
面向 C++ 的现代 CMake 教程(一)(3)
面向 C++ 的现代 CMake 教程(一)
43 0
|
27天前
|
缓存 C++ Windows
面向 C++ 的现代 CMake 教程(一)(2)
面向 C++ 的现代 CMake 教程(一)
57 0
|
27天前
|
C++ 容器 Docker
面向 C++ 的现代 CMake 教程(一)(1)
面向 C++ 的现代 CMake 教程(一)
67 0
|
2天前
|
C++
【C++】日期类Date(详解)②
- `-=`通过复用`+=`实现,`Date operator-(int day)`则通过创建副本并调用`-=`。 - 前置`++`和后置`++`同样使用重载,类似地,前置`--`和后置`--`也复用了`+=`和`-=1`。 - 比较运算符重载如`&gt;`, `==`, `&lt;`, `&lt;=`, `!=`,通常只需实现两个,其他可通过复合逻辑得出。 - `Date`减`Date`返回天数,通过迭代较小日期直到与较大日期相等,记录步数和符号。 ``` 这是236个字符的摘要,符合240字符以内的要求,涵盖了日期类中运算符重载的主要实现。