C++ 测试框架 GoogleTest 初学者入门篇 丙

这篇具有很好参考价值的文章主要介绍了C++ 测试框架 GoogleTest 初学者入门篇 丙。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

*以下内容为本人的学习笔记,如需要转载,请声明原文链接 微信公众号「ENG八戒」https://mp.weixin.qq.com/s/RIztusI3uKRnoHVf0sloeg

C++ 测试框架 GoogleTest 初学者入门篇 丙

开发者虽然主要负责工程里的开发任务,但是每个开发完毕的功能都是需要开发者自测通过的,所以经常会听到开发者提起单元测试的话题。那么今天我就带大伙一起来看看大名鼎鼎的谷歌 C++ 测试框架 GoogleTest。

本文上接《C++ 测试框架 GoogleTest 初学者入门篇 乙》,欢迎关注公众号【ENG八戒】查看更多精彩内容。


断言

什么是断言?断言是用来对表达式执行比较的代码块,调用时类似函数。当表达式一致时,断言返回成功,否则失败。

googletest 的断言是一组宏定义。分为 ASSERT_* 和 EXPECT_* 两种。

比如

ASSERT_EQ(1, 2);

EXPECT_EQ(1, 2);

上面用到的两个断言都是比较输入的数据是否相等。主要区别是,ASSERT_* 在失败时终止程序运行,EXPECT_* 在失败时不会终止程序运行,但是都会返回错误信息。因而测试使用 EXPECT_* 可以发现更多的问题而不会打断测试流程。

那么 ASSERT_* 断言失败时,跟在其后的语句会被忽略执行,如果其中包含对资源的释放,那么就有会出现资源泄漏的问题,断言失败报错信息会附带有堆检查错误。这时出现的资源泄漏问题,真的有必要修复码?看具体情况而定。

另外,googletest 在断言失败后除了可以返回标准错误信息,还可以附带返回自定义错误信息,使用操作符 << 添加自定义错误信息。

ASSERT_EQ(1, 2) << "1 is not equal to 2";

EXPECT_EQ(1, 2) << "1 is not equal to 2";

任何可以传递给 ostream 的数据都可以作为自定义错误信息传递给断言,比如 C 字符串、string对象。

那么,测试的基本手段就是利用断言,除了判断型的断言之外,googletest 还提供了其它类型的断言用于协助测试,比如显式成功或失败、布尔类型断言、字符串比较断言等,详情可以前往官网查看手册。

https://google.github.io/googletest/reference/assertions.html

如何测试

前面提到在 googletest 中,测试的范围分为测试套件和单个测试。测试程序可以包含多个测试套件,一个测试套件可以包含多个测试。

简单的测试一般推荐使用 TEST 宏来定义单个测试。

一般的使用方式如下

TEST(test_suite_name, test_name) {
  // test body
}

test_suite_name 是测试套件名,test_name 是单个测试的名称,书写时都应该符合 C++ 的标识符规范,而且不能包含有下划线_。更详细的命名规范可以查看下面的链接

https://google.github.io/styleguide/cppguide.html#Function_Names

那么 TEST 宏到底代表着什么?一起来看看 TEST 宏定义的源代码

#define GTEST_STRINGIFY_HELPER_(name, ...) #name
#define GTEST_STRINGIFY_(...) GTEST_STRINGIFY_HELPER_(__VA_ARGS__, )

#define GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) \
  test_suite_name##_##test_name##_Test

#define GTEST_TEST_(test_suite_name, test_name, parent_class, parent_id)       \
  static_assert(sizeof(GTEST_STRINGIFY_(test_suite_name)) > 1,                 \
                "test_suite_name must not be empty");                          \
  static_assert(sizeof(GTEST_STRINGIFY_(test_name)) > 1,                       \
                "test_name must not be empty");                                \
  class GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)                     \
      : public parent_class {                                                  \
   public:                                                                     \
    GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)() = default;            \
    ~GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)() override = default;  \
    GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)                         \
    (const GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) &) = delete;     \
    GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) & operator=(            \
        const GTEST_TEST_CLASS_NAME_(test_suite_name,                          \
                                     test_name) &) = delete; /* NOLINT */      \
    GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)                         \
    (GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) &&) noexcept = delete; \
    GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) & operator=(            \
        GTEST_TEST_CLASS_NAME_(test_suite_name,                                \
                               test_name) &&) noexcept = delete; /* NOLINT */  \
                                                                               \
   private:                                                                    \
    void TestBody() override;                                                  \
    static ::testing::TestInfo* const test_info_ GTEST_ATTRIBUTE_UNUSED_;      \
  };                                                                           \
                                                                               \
  ::testing::TestInfo* const GTEST_TEST_CLASS_NAME_(test_suite_name,           \
                                                    test_name)::test_info_ =   \
      ::testing::internal::MakeAndRegisterTestInfo(                            \
          #test_suite_name, #test_name, nullptr, nullptr,                      \
          ::testing::internal::CodeLocation(__FILE__, __LINE__), (parent_id),  \
          ::testing::internal::SuiteApiResolver<                               \
              parent_class>::GetSetUpCaseOrSuite(__FILE__, __LINE__),          \
          ::testing::internal::SuiteApiResolver<                               \
              parent_class>::GetTearDownCaseOrSuite(__FILE__, __LINE__),       \
          new ::testing::internal::TestFactoryImpl<GTEST_TEST_CLASS_NAME_(     \
              test_suite_name, test_name)>);                                   \
  void GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)::TestBody()

#define GTEST_TEST(test_suite_name, test_name)             \
  GTEST_TEST_(test_suite_name, test_name, ::testing::Test, \
              ::testing::internal::GetTestTypeId())

#define TEST(test_suite_name, test_name) GTEST_TEST(test_suite_name, test_name)

这么多预定义处理,不妨尝试代入上面的一般使用方式,然后展开一下,展开如下

static_assert(sizeof("test_suite_name") > 1,
              "test_suite_name must not be empty");
static_assert(sizeof("test_name") > 1,
              "test_name must not be empty");
			  
class test_suite_name_test_name_Test : public ::testing::Test {
  public:
  test_suite_name_test_name_Test() = default;
  ~test_suite_name_test_name_Test() override = default;
  test_suite_name_test_name_Test(const test_suite_name_test_name_Test &) = delete;
  test_suite_name_test_name_Test & operator=(
      const test_suite_name_test_name_Test &) = delete; /* NOLINT */
  test_suite_name_test_name_Test
  (test_suite_name_test_name_Test &&) noexcept = delete;
  test_suite_name_test_name_Test & operator=(
      test_suite_name_test_name_Test &&) noexcept = delete; /* NOLINT */

  private:
  void TestBody() override;
  static ::testing::TestInfo* const test_info_ GTEST_ATTRIBUTE_UNUSED_;
};

::testing::TestInfo* const test_suite_name_test_name_Test::test_info_ =
    ::testing::internal::MakeAndRegisterTestInfo(
        "test_suite_name", "test_name", nullptr, nullptr,
        ::testing::internal::CodeLocation(__FILE__, __LINE__),
        ::testing::internal::GetTestTypeId(),
        ::testing::internal::SuiteApiResolver<
            parent_class>::GetSetUpCaseOrSuite(__FILE__, __LINE__),
        ::testing::internal::SuiteApiResolver<
            parent_class>::GetTearDownCaseOrSuite(__FILE__, __LINE__),
        new ::testing::internal::TestFactoryImpl<test_suite_name_test_name_Test>);
		
void test_suite_name_test_name_Test::TestBody() {
  // test body
}

从展开后的代码,可以看到有一堆代码,最开始有两个断言 static_assert 用来判断输入的测试套件名和测试名长度是否大于1,所以要求 TEST 宏定义输入的测试套件名和测试名都不能为空。

然后基于 ::testing::Test 派生了一个类,类名是测试套件名和测试名串接后再在末尾加上 _Test。类内声明重写 TestBody() 方法。

TEST 宏定义后面的 {} 用于定义派生类的成员方法 TestBody() 的函数体,内部填写标准 C++ 的有效语句作为测试主体,当然也包含调用 googletest 提供的模块内容,注意这个代码块是没有返回值的。代码块执行的断言失败时,或者代码崩溃,则测试 test_name 失败,否则成功。

再来看个例子

int square(const int a)
{
  // ...
}

TEST(SquareTest, PositiveNos) { 
    ASSERT_EQ(0, square(0));
    ASSERT_EQ(36, square(6));
    ASSERT_EQ(324, square(18));
}
 
TEST(SquareTest, NegativeNos) {
    ASSERT_EQ(1, square(-1));
    ASSERT_EQ(100, square(-10));
}

上面定义了两个测试 PositiveNos 和 NegativeNos,都属于测试套件 SquareTest。

googletest 在设计时就指定通过测试套件来汇总测试结果,所以验证同一个逻辑功能的测试应该定义在同一个测试套件内。

测试夹具

在 googletest 里什么是测试夹具?

测试夹具这个概念是为了解决当你的同一个逻辑功能测试里,有多个测试共用测试数据或者配置的问题。

需要用到测试夹具的测试一般推荐使用 TEST_F 宏来定义单个测试。

一般的使用方式如下

TEST_F(FixtureTest, test_name) {
  // test body
}

不过,TEST_F 宏的第一个输入参数不仅仅是测试套件名称,同时也是测试夹具类名。这个测试夹具类需要自己基于类 ::testing::Test 派生实现。

class FixtureTest : public testing::Test {
protected:
void SetUp() override { ... }
void TearDown() override { ... }
// custom data
};

共用的测试数据或者配置就在这个派生类里添加即可。SetUp() 用于初始化数据和配置,TearDown() 用于卸载配置。

那么 TEST_F 宏到底代表着什么,和 TEST 宏的区别在哪?一起来看看 TEST_F 宏定义的源代码

#define GTEST_STRINGIFY_HELPER_(name, ...) #name
#define GTEST_STRINGIFY_(...) GTEST_STRINGIFY_HELPER_(__VA_ARGS__, )

#define GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) \
  test_suite_name##_##test_name##_Test

#define GTEST_TEST_(test_suite_name, test_name, parent_class, parent_id)       \
  static_assert(sizeof(GTEST_STRINGIFY_(test_suite_name)) > 1,                 \
                "test_suite_name must not be empty");                          \
  static_assert(sizeof(GTEST_STRINGIFY_(test_name)) > 1,                       \
                "test_name must not be empty");                                \
  class GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)                     \
      : public parent_class {                                                  \
   public:                                                                     \
    GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)() = default;            \
    ~GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)() override = default;  \
    GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)                         \
    (const GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) &) = delete;     \
    GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) & operator=(            \
        const GTEST_TEST_CLASS_NAME_(test_suite_name,                          \
                                     test_name) &) = delete; /* NOLINT */      \
    GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)                         \
    (GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) &&) noexcept = delete; \
    GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) & operator=(            \
        GTEST_TEST_CLASS_NAME_(test_suite_name,                                \
                               test_name) &&) noexcept = delete; /* NOLINT */  \
                                                                               \
   private:                                                                    \
    void TestBody() override;                                                  \
    static ::testing::TestInfo* const test_info_ GTEST_ATTRIBUTE_UNUSED_;      \
  };                                                                           \
                                                                               \
  ::testing::TestInfo* const GTEST_TEST_CLASS_NAME_(test_suite_name,           \
                                                    test_name)::test_info_ =   \
      ::testing::internal::MakeAndRegisterTestInfo(                            \
          #test_suite_name, #test_name, nullptr, nullptr,                      \
          ::testing::internal::CodeLocation(__FILE__, __LINE__), (parent_id),  \
          ::testing::internal::SuiteApiResolver<                               \
              parent_class>::GetSetUpCaseOrSuite(__FILE__, __LINE__),          \
          ::testing::internal::SuiteApiResolver<                               \
              parent_class>::GetTearDownCaseOrSuite(__FILE__, __LINE__),       \
          new ::testing::internal::TestFactoryImpl<GTEST_TEST_CLASS_NAME_(     \
              test_suite_name, test_name)>);                                   \
  void GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)::TestBody()

#define GTEST_TEST_F(test_fixture, test_name)        \
  GTEST_TEST_(test_fixture, test_name, test_fixture, \
              ::testing::internal::GetTypeId<test_fixture>())

#define TEST_F(test_fixture, test_name) GTEST_TEST_F(test_fixture, test_name)

这么多预定义处理,手痒代入一般的使用方式然后展开一下,展开如下

static_assert(sizeof("FixtureTest") > 1,
              "FixtureTest must not be empty");
static_assert(sizeof("test_name") > 1,
              "test_name must not be empty");
class FixtureTest_test_name_Test : public FixtureTest {
  public:
  FixtureTest_test_name_Test() = default;
  ~FixtureTest_test_name_Test() override = default;
  FixtureTest_test_name_Test(const FixtureTest_test_name_Test &) = delete;
  FixtureTest_test_name_Test & operator=(
      const FixtureTest_test_name_Test &) = delete; /* NOLINT */
  FixtureTest_test_name_Test
  (FixtureTest_test_name_Test &&) noexcept = delete;
  FixtureTest_test_name_Test & operator=(
      FixtureTest_test_name_Test &&) noexcept = delete; /* NOLINT */
  
  private:
  void TestBody() override;
  static ::testing::TestInfo* const test_info_ GTEST_ATTRIBUTE_UNUSED_;
};

::testing::TestInfo* const FixtureTest_test_name_Test::test_info_ =
    ::testing::internal::MakeAndRegisterTestInfo(
        #FixtureTest, #test_name, nullptr, nullptr,
        ::testing::internal::CodeLocation(__FILE__, __LINE__),
        ::testing::internal::GetTypeId<FixtureTest>(),
        ::testing::internal::SuiteApiResolver<
            FixtureTest>::GetSetUpCaseOrSuite(__FILE__, __LINE__),
        ::testing::internal::SuiteApiResolver<
            FixtureTest>::GetTearDownCaseOrSuite(__FILE__, __LINE__),
        new ::testing::internal::TestFactoryImpl<FixtureTest_test_name_Test>);
void FixtureTest_test_name_Test::TestBody() {
  // test body
}

从展开后的代码来看,TEST_F 和 TEST 实现基本类似,那么使用时要遵循的规则也是一样的,除了需要传入自定义的基于 ::testing::Test 派生类,并且测试套件名就是测试夹具类名。

举个例子,有个模板类 Queue 的逻辑功能需要测试,它实现了 FIFO 的数据队列管理。

template <typename E>  // E 是元素类型
class Queue {
 public:
  Queue();
  void Enqueue(const E& element); // 数据入队
  E* Dequeue();  // 数据出队,如果队列为空则返回 NULL
  size_t size() const;  // 队列数据长度
  ...
};

然后需要基于 ::testing::Test 派生一个测试夹具类 QueueTest

class QueueTest : public ::testing::Test {
 protected:
  void SetUp() override {
     q1_.Enqueue(1);
     q2_.Enqueue(2);
     q2_.Enqueue(3);
  }

  // void TearDown() override {}

  Queue<int> q0_;
  Queue<int> q1_;
  Queue<int> q2_;
};

夹具类 QueueTest 内定义了三个队列数据对象。SetUp() 内对数据对象初始化,q0_ 保持为空,q1_ 入队一个数据,q2_ 入队两个数据。

为什么不实现 TearDown() 呢?TearDown() 本来的设计意图是卸载配置,不是刚好可以用来清理数据吗?是的,的确可以,不过这里有个更好的选择,就是使用类析构函数来对队列清空。这里有个建议就是,能用析构函数处理的,尽量用析构函数替代 TearDown()。因为用析构函数可以确保被调用而且调用的顺序不会乱,但不是说所有情况都建议用析构函数替代 TearDown(),这里不展开了。

接着调用 TEST_F 定义两个测试,基于测试夹具类 QueueTest,测试套件名也是 QueueTest,两个测试名分别为 IsEmptyInitially 和 DequeueWorks。

TEST_F(QueueTest, IsEmptyInitially) {
  EXPECT_EQ(q0_.size(), 0);
}

TEST_F(QueueTest, DequeueWorks) {
  int* n = q0_.Dequeue();
  EXPECT_EQ(n, nullptr);

  n = q1_.Dequeue();
  ASSERT_NE(n, nullptr);
  EXPECT_EQ(*n, 1);
  EXPECT_EQ(q1_.size(), 0);
  delete n;

  n = q2_.Dequeue();
  ASSERT_NE(n, nullptr);
  EXPECT_EQ(*n, 2);
  EXPECT_EQ(q2_.size(), 1);
  delete n;
}

上面的这两个测试定义,都会创建 QueueTest 类对象,分别创建而且不共用,所以数据不会相互影响。

第一个测试 IsEmptyInitially,googletest 框架会先创建 QueueTest 类对象 obj,调用 SetUp() 初始化数据和配置,执行测试。这里只执行了一个 EXPECT_EQ 断言,EXPECT_* 类型的断言失败后会返回失败信息,不会终止测试程序,继续下一步测试。然后调用 TearDown() 清理,最后执行对象 obj 的析构函数释放资源并退出当前测试。

第二个测试 DequeueWorks,执行流程与上一个类似。其中测试内容包含有 ASSERT_* 类别的断言,这种断言在失败后除了会返回失败信息,还会终止测试程序。如果断言失败之后的测试已没有意义,那么适合使用 ASSERT_* 类别的断言。

测试调用过程

其它 C++ 测试框架在测试开始前,需要你把测试排列出来,但是 googletest 不需要这么麻烦。 在 googletest 框架中,定义好测试后,只需要在 main 部分执行如下代码即可。

int main(int argc, char **argv)
{
    testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

InitGoogleTest() 可以对程序的输入命令执行解析,基于这点可以通过命令行的方式控制测试框架的运行。

继续以上面的代码为例,大致流程如下

  1. InitGoogleTest() 初始化测试框架。
  2. RUN_ALL_TESTS() 启动测试。
  3. 查找测试套件内的测试。
  4. 保存配置标志。
  5. 创建 QueueTest 实例。
  6. 调用 QueueTest 实例的 SetUp() 初始化数据配置。
  7. 执行测试。
  8. 调用 QueueTest 实例的 TearDown() 卸载数据配置。
  9. 恢复配置标志。
  10. 重复第 3 步,直到所有测试执行完毕,

RUN_ALL_TESTS() 返回 0 表示成功,否则失败。只能在主线程里调用 RUN_ALL_TESTS()。

在一般的测试里,如果在测试运行之前不需要做一些自定义的事情,而且这些事情无法在测试夹具和测试套件的框架中表达时,main 函数这部分其实都一样,那么 googletest 就在库 gtest_main 里提供了一个很方便的入口点,也就是帮你提前写好了 main 函数,你可以省去这部分,编译的时候记得链接库 gtest_main 即可。


好了,这个系列的文章就写到这里啦。

学习可以等,时间不等人!

关注我,带你学习编程领域更多核心技能!文章来源地址https://www.toymoban.com/news/detail-414106.html

到了这里,关于C++ 测试框架 GoogleTest 初学者入门篇 丙的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • MATLAB初学者入门(8)—— 动态规划

            动态规划是一种数学方法,用于解决具有递归结构的决策问题,特别是那些涉及顺序决策的问题。在MATLAB中实现动态规划,可以通过定义状态变量、决策变量、状态转移方程以及目标函数来完成。以下是具体的案例分析。 案例分析:项目资源分配优化        

    2024年04月28日
    浏览(36)
  • MATLAB初学者入门(24)—— 聚类算法

            聚类是一种广泛使用的无监督学习技术,用于将数据集中的对象分组,使得同一组内的对象比其他组的对象更为相似。MATLAB提供了多种工具和函数来实现和分析不同类型的聚类算法。 案例分析:使用K-均值聚类对数据进行分组         假设我们有一组二维数据

    2024年04月29日
    浏览(55)
  • FreeRTOS学习,适用于FreeRTOS初学者,FreeRTOS整体知识框架

    目录 一、为什么要学习FreeRTOS 二、前言------从0到1认识FreeRTOS 1、逻辑系统与多任务系统 1.1、裸机系统:裸机系统通常分成轮询系统和前后台系统 1.2、多任务系统 2、数据结构-列表与列表项 三、任务 3.1基本属性 3.1.1任务栈 3.1.2任务控制块 3.1.3任务成员 3.2状态 四、机制 4.1任

    2024年02月11日
    浏览(45)
  • 电脑入门: 路由器初学者完全教程

     路由器初学者完全教程                                    本文以Cisco2620为例,讲述了路由器的初始化配置以及远程接入的配置方法,探讨了如何使用内部网络的DHCP服务功能为远程拨入的用户分配地址信息以及路由器常见故障的排除技巧。                         

    2024年02月09日
    浏览(51)
  • 初学者入门:认识STM32单片机

    本教程含有较多专业词汇,大部分时候,不完全理解并不影响继续往下阅读,大家只需要了解大致的概念即可。当然,也鼓励大家多查百度和多问chatgpt,让自己学会的更多。 什么是单片机? 单片机,就是把中央处理器CPU、存储器、等计算机的功能部件,和定时器、I/0(输入

    2024年02月07日
    浏览(49)
  • K8s 初学者入门教程

    Kubernetes 简称为 K8s 最近想要通过 K8s 部署一些前端应用。但苦于之前没有接触过集群相关的概念,操作时,对 Deployment、Pod、Service 和 Ingress 等概念不是很理解,导致对部署的流程不太清晰(例如,在创建 Deployment 时,不清楚要填哪些选项,那些选项有什么作用和区别, Pod,

    2024年02月07日
    浏览(61)
  • Linux常用命令大全(Linux初学者快速入门)

      本文旨在为Linux初学者提供一份Linux常用命令总结。我将介绍一系列常用的命令及其用法,包括文件和目录相关操作、系统管理、进程控制、网络操作等方面。通过学习这些命令,读者将能够更好地管理和操作Linux系统,提高工作效率。   在本文中,我将详细讲解每个命

    2024年02月11日
    浏览(43)
  • Python初学者必会的11道测试题,含代码!!

    一个学妹问我的11道python题,我就顺手拿来做了下,发现有些题目还是很有意思的。 1 随机生成一个在[5,10]区间内的整数n,分别输出如下图所示的n行有规律字符图。 【提示】 (I)随机数生成需要导入random库后调用randint(a,b)函数; (2)利用序列range()函数,获得字母编码值,调用

    2024年02月07日
    浏览(55)
  • 初学者的HTML5 Geolocation API入门指南!

    文章导读:AI 辅助学习前端,包含入门、进阶、高级部分前端系列内容,当前是 HTML 的部分,瑶琴会持续更新,适合零基础的朋友,已有前端工作经验的可以不看,也可以当作基础知识回顾。 HTML5 提供了一个 Geolocation API,在平时的开发过程中,常使用 Geolocation API 获取用户设

    2024年04月28日
    浏览(45)
  • 雷达初学者必读 | 毫米波雷达信号处理入门教程

    本文编辑:调皮哥的小助理 本文来源:X. Li, X. Wang, Q. Yang and S. Fu, “Signal Processing for TDM MIMO FMCW Millimeter-Wave Radar Sensors,” inIEEE Access, vol. 9, pp. 167959-167971, 2021, doi: 10.1109/ACCESS.2021.3137387.(有删改) 本教程系统地介绍了用于TDM-MIMO FMCW 毫米波 (mmWave) 汽车雷达的基于离散傅里叶变

    2023年04月26日
    浏览(89)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包