编程知识点

本文最后更新于 2025年1月8日 上午

本文主要分享了编程过程中积累的知识点。

编译与卸载

ROS编译与运行

编写CmakeLists.txt

CmakeLists.txt里指定第三方库所在的路径

CmakeLists.txt里指定第三方库所在的路径,即指定其编译安装后.cmake文件所在的路径,例如:

1
2
3
4
5
6
7
8
# 指定OpenCVConfig.cmake文件的目录
# 注意opencv安装目录下的/share/OpenCV
set(OpenCV_DIR /usr/local/share/OpenCV) # 默认安装在/usr/local/
set(OpenCV_DIR /usr/local/opencv/opencv345/share/OpenCV) # 自定义安装在/usr/local/opencv/opencv345的openv3.4.5
set(OpenCV_DIR /usr/local/opencv/opencv452/lib/cmake/opencv4)
# set(OpenCV_DIR /opt/ros/kinetic/share/OpenCV-3.3.1-dev)
# set(OpenCV_DIR /path/to/opencv/build)
find_package(OpenCV REQUIRED)
1
find_library(flowfilter_gpu_LIBS NAMES flowfilter_gpu PATHS /usr/local/include)

首先,set(OpenCV_DIR /usr/local/opencv/opencv345/share/OpenCV)这行代码设置了一个变量OpenCV_DIR,它指向OpenCV库的安装位置。在这个例子中,OpenCV库被安装在/usr/local/opencv/opencv345/share/OpenCV这个路径下。OpenCV_DIR这个变量通常用于指定OpenCV的cmake配置文件的位置,这个文件包含了OpenCV库的版本信息、编译选项等信息。

然后,find_package(OpenCV REQUIRED)这行代码告诉CMake去查找OpenCV库。REQUIRED关键字表示如果CMake不能找到OpenCV库,那么CMake应该停止配置过程并显示错误信息。如果CMake成功找到了OpenCV库,那么它将设置一些变量,例如OpenCV_INCLUDE_DIRSOpenCV_LIBS,这些变量分别包含了OpenCV的头文件路径和库文件路径,可以在后续的target_include_directoriestarget_link_libraries命令中使用。

如果没有设置OpenCV_DIRfind_package命令会在默认的路径下查找OpenCV库。这些默认的路径包括:

  • CMake的模块路径(CMAKE_MODULE_PATH
  • CMake的安装前缀(CMAKE_PREFIX_PATH
  • 系统的环境变量路径

具体来说,find_package会查找名为OpenCVConfig.cmakeopencv-config.cmake的文件,这个文件通常位于OpenCV库的安装目录中。

如果你的OpenCV库安装在非标准的位置,或者你有多个版本的OpenCV库并且想要选择一个特定的版本,那么你可以通过设置OpenCV_DIR来指定OpenCV库的路径。如果没有设置OpenCV_DIR,CMake可能会找到错误的版本或者找不到OpenCV库。

OpenCV_DIROpenCV_INCLUDE_DIR是两个不同的变量,它们在CMake中的作用也不同。

  • OpenCV_DIR是用于指定OpenCV的cmake配置文件位置的变量。find_package命令会使用OpenCV_DIR变量的值作为查找OpenCV配置文件的起始路径。如果OpenCV_DIR被设置,find_package就会直接在这个路径下查找配置文件,而不会在其他路径下查找。

  • OpenCV_INCLUDE_DIR通常是find_package命令找到OpenCV库后设置的一个变量,它包含了OpenCV的头文件路径。这个变量通常用于target_include_directories命令,以便你的项目可以找到OpenCV的头文件。

如果你设置了OpenCV_INCLUDE_DIR,而不是OpenCV_DIR,然后调用find_package(OpenCV REQUIRED),那么find_package命令可能无法找到正确的OpenCV库,因为它不知道在哪里查找OpenCV的配置文件。在这种情况下,find_package命令可能会找到错误的OpenCV版本,或者找不到OpenCV库。find_package命令找到的结果会覆盖你设置的OpenCV_INCLUDE_DIR变量。

总的来说,如果你想要指定OpenCV库的位置,你应该设置OpenCV_DIR,而不是OpenCV_INCLUDE_DIR

(在ROS中)编译第三方开源软件需要下载的问题

注意CmakeList.txt里有没有指定具体版本。在package.xml里也可以看到指定的版本。

其实就类似于在系统中cmakemakemake install的步骤,只不过这里的第三方库是安装在了ROS工作区里被相互调用,catkin clean后也就删除掉了,而没有安装在系统环境里。也方便使用指定版本的第三方库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# URL https://github.com/gflags/gflags/archive/v2.2.1.zip

# GIT_REPOSITORY https://github.com/jbeder/yaml-cpp
# GIT_TAG ${YAML_CPP_TAG}

# DOWNLOAD_COMMAND rm -f SuiteSparse-${VERSION}.tar.gz && wget --retry-connrefused --waitretry=1 --timeout=40 --tries 3 https://github.com/ethz-asl/thirdparty_library_binaries/raw/master/SuiteSparse-${VERSION}.tar.gz

# 对于上述的3种情况,可以替换为:
DOWNLOAD_COMMAND ""
URL "/yourfolder/gflags-2.2.1.zip" # 使用存档时,它将自动解压缩,除非设置了 DOWNLOAD_NO_EXTRACT 选项来阻止它。
# 详细示例:https://zeyulong.com/posts/6d95d81a/

# 默认解压路径为catkin_ws/build/xxx/xxx_src-prefix/src/xxx_src

# 可选
URL_MD5 4628df9eeae10ae5f0c486f1ac982fce # (可选,仅用作文件验证)与URL搭配。随便输入一个MD5码,编译时会报错当前压缩包正确的MD5码。
UPDATE_COMMAND "" # 与URL搭配。使用自定义命令覆盖下载方法的更新步骤。
DOWNLOAD_NO_EXTRACT TRUE # 允许通过为此选项传递一个布尔真值来禁用下载步骤的提取部分。
# 如果希望控制下载的归档文件的位置及其名称,可以使用以下选项
set(CMAKE_CURRENT_BINARY_DIR /yourfolder) # 放在主程序中,而不包含在ExternalProject_Add()
DOWNLOAD_DIR ${CMAKE_CURRENT_BINARY_DIR}
DOWNLOAD_NAME jsoncpp_1.8.4.tar.gz

或者,将src(这个文件是原本解压下载的第三方源码source的地方,具体名称要看CMakeLists.txt中SOURCE_DIR的设置)中的各个第三方源码都解压好,放到src对应的文件夹中。例如catkin_ws/build/xxx/xxx_src-prefix/src/xxx.tar.gz

cmake

一般流程

1
2
3
4
cd package_name
mkdir build
cd build
cmake ..

我个人推荐把第三方库安装在/usr/local文件夹下进行管理。例如,在/usr/local文件夹下新建文件夹eigen3,后在eigen3文件夹下新建文件夹eigen330eigen340

定义编译参数

可在cmake命令后加参数:

1
2
3
4
# 指定安装目录。使用该参数可以指定安装目录,使得在执行make install命令时,生成的可执行文件、库文件、头文件等可以被安装到指定的目录下。
cmake -DCMAKE_INSTALL_PREFIX=/usr/local/package_name ..
# 指定使用的gcc和g++版本
-DCMAKE_C_COMPILER=gcc-10 -DCMAKE_CXX_COMPILER=g++-10

也可以在CMakeLists.txt文件中定义,例如,启用参数EFFT_USE_FFTW3

1
cmake DEFFT_USE_FFTW3 ..

或,在CMakeLists.txt文件中:

1
target_compile_definitions(efft-unit-tests PRIVATE EFFT_USE_FFTW3)

保存cmake输出

ROS: cmake build的输出在catkin_ws/logs/your_package_name/build.cmake.log、build.make.log里。

cmake命令的输出信息通常在终端中显示,而不是保存在文件中。这些信息包括配置过程中的警告、错误以及其他重要信息。

然而,你可以将cmake命令的输出重定向到一个文件中。例如,你可以使用以下命令将输出保存到一个名为output.txt的文件中:

1
cmake .. > output.txt

在这个命令中,>操作符将cmake命令的输出重定向到output.txt文件中。如果output.txt文件已经存在,这个命令将覆盖它的内容。如果你想要追加输出到文件中,而不是覆盖它,你可以使用>>操作符,如下所示:

1
cmake .. >> output.txt

请注意,这些命令只会捕获标准输出,而不会捕获错误输出。如果你也想要捕获错误输出,你可以使用2>&1,如下所示:

1
cmake .. > output.txt 2>&1

在这个命令中,2>&1将错误输出重定向到标准输出,然后>操作符将标准输出重定向到output.txt文件中。这样,output.txt文件将包含所有的输出,包括错误信息。

make

1
2
3
4
5
# 建议在make之前先查看CPU的核心数
nproc # 你可以根据你的核心数来调整make指令的参数,比如笔者的CPU核心数为12,则可以执行:
make -j12
make # 根据Makefile编译源代码,连接,生成目标文件,可执行文件。
make install # 将编译成功的可执行文件安装到系统目录中,一般为/usr/local/bin目录。

cmake --install .make install的区别:

cmake --install .make install都是用来安装编译好的程序的命令,但它们在使用的构建系统和工作方式上有所不同。

  • make install是GNU Make的命令,它依赖于Makefile中的install目标。这个install目标通常会将编译好的二进制文件、库文件、头文件等复制到系统的指定位置,如/usr/local/bin/usr/local/lib等。这个命令通常在使用GNU Autotools或者手写Makefile的项目中使用。

  • cmake --install .是CMake的命令,它会执行CMakeLists.txt文件中定义的安装规则。这个命令在CMake 3.15及以后的版本中可用,它是cmake -P cmake_install.cmake的一个更简洁的替代。这个命令的好处是它不依赖于特定的构建系统,可以在任何CMake支持的构建系统中使用。

总的来说,这两个命令的功能是类似的,但cmake --install .更加通用,不依赖于特定的构建系统。

卸载

如果因为反复./configure xx然后make会导致安装路径混乱,sudo make install失败。所以如果路径设错了又已经make完,需要make clean来清除一下。如果想把自己刚刚make install安装的卸载掉,可以在那个目录里直接用sudo make uninstall

1
2
3
4
5
6
7
8
9
10
11
12
13
cd package_name/build

# 卸载使用make install命令安装到系统路径的文件
# `install_manifest.txt`文件里包含了所有安装的文件的路径
cat install_manifest.txt | sudo xargs rm # 或
sudo xargs rm < install_manifest.txt
# 上述命令只会删除掉文件夹里的文件,会留下空文件夹。所以可以继续对照`install_manifest.txt`文件,手动删掉多余的空文件夹。

sudo make uninstall
sudo make clean #清除上一次make命令生成的文件
sudo make distclean #清除上一次make以及configure命令生成的文件
cd ..
sudo rm -r build

make uninstall 是一个常见的 makefile 目标,它的主要作用是删除由 make install 命令安装的文件。

当你运行 make install 命令时,通常会将一些文件(例如可执行文件、库文件、头文件等)复制到系统的某些目录下(例如 /usr/local/bin/usr/local/lib 等)。make uninstall 命令就是用来删除这些文件的。

然而,需要注意的是,并不是所有的 makefile 都提供 make uninstall 目标。如果 makefile 没有提供这个目标,运行 make uninstall 命令将会导致错误。

在运行 make uninstall 命令之前,你应该查看 makefile 或者相关的文档,以确认这个命令是否可用,以及它会删除哪些文件。

cat install_manifest.txt | sudo xargs rmsudo make uninstall 都是用来删除由 make install 命令安装的文件的。然而,它们的工作方式有所不同。

  • cat install_manifest.txt | sudo xargs rm:这个命令会读取 install_manifest.txt 文件,这个文件通常由 make install 命令生成,包含了所有被安装的文件的列表。然后,它会使用 xargs rm 命令删除这些文件。这个命令不依赖于 makefile,只要 install_manifest.txt 文件存在,就可以使用。
  • sudo make uninstall:这个命令会执行 makefile 中的 uninstall 目标。这个目标通常会删除所有被 make install 命令安装的文件。然而,需要注意的是,并不是所有的 makefile 都提供 uninstall 目标。如果 makefile 没有提供这个目标,运行 sudo make uninstall 命令将会导致错误。

总的来说,这两个命令的功能是相似的,但是 cat install_manifest.txt | sudo xargs rm 命令更为直接,不依赖于 makefile。而 sudo make uninstall 命令则需要 makefile 提供 uninstall 目标。

make cleanmake distclean 是两个常见的 makefile 目标,它们的功能取决于 makefile 的编写者如何定义它们。但是,通常它们的功能如下:

  • make clean:这个命令通常用于删除所有由 makefile 生成的文件。这通常包括编译产生的对象文件(.o 或 .obj 文件)和编译器生成的中间文件。但是,它通常不会删除配置文件或者 makefile 文件。
  • make distclean:这个命令通常用于将目录恢复到初始状态。除了删除 make clean 会删除的文件,它还会删除配置文件和 makefile 文件。这个命令通常在你想要重新配置和编译一个项目时使用。

需要注意的是,这两个命令的具体行为取决于 makefile 的编写者。在使用这些命令之前,你应该查看 makefile 或者相关的文档,以了解这些命令的具体行为。

编程命名规范

  1. 匈牙利命名法(将变量类型写进变量名的命名方法)。

    其基本原则是,变量名=属性+类型+对象描述。通过在变量名前面加上相应的小写字母的符号标识作为前缀,标识出变量的作用域,类型等。

    这些符号可以多个同时使用,顺序是先m_(成员变量),再指针,再简单数据类型,再其他。例如:m_lpsStr,表示指向一个字符串的长指针成员变量。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    # 前缀类型
    a 数组(Array)
    b 布尔值(Boolean)
    by 字节(Byte)
    c 有符号字符(Char)
    cb 无符号字符(Char Byte,并没有神马人用的)
    cr 颜色参考值(Color Ref)
    cx,cy 坐标差(长度 Short Int)
    dw 双字(Double Word)
    fn 函数(Function)
    h Handle(句柄)
    i 整形(Int)
    l 长整型(Long Int)
    lp 长指针(Long Pointer)
    m_ 类成员(Class Member)
    n 短整型(Short Int)
    np 近程指针(Near Pointer)
    p 指针(Pointer)
    s 字符串(String)
    sz 以 Null 做结尾的字符串型(String with Zero End)
    w 字(Word)
  2. 驼峰式命名法,又叫小驼峰式命名法。常用于变量名,函数名。

    要求第一个单词首字母小写,后面其他单词首字母大写。

    1
    2
    3
    int myAge;
    char myName[10];
    float manHeight;
  3. 帕斯卡命名法,又叫大驼峰式命名法。常用于类名,属性,命名空间等。

    与小驼峰式命名法的最大区别在于,每个单词的第一个字母都要大写。

    1
    2
    3
    int MyAge;
    char MyName[10];
    float ManHeight;
  4. 下划线命名法。

    下划线命名法并不如大小驼峰式命名法那么备受推崇,但是也是浓墨重彩的一笔。尤其在宏定义和常量中使用比较多,通过下划线来分割全部都是大写的单词。还有变量名太长的变量

    该命名规范,也是很简单,要求单词与单词之间通过下划线连接即可。

    1
    2
    3
    int my_age;
    char my_name[10];
    float man_height;
  5. 在C++编程中,变量名后加_的命名方式通常用于表示类的私有数据成员。这是一种命名约定,用于区分类的数据成员和局部变量,以提高代码的可读性。例如,fftwInput_fftwOutput_plan_都是类的私有数据成员,它们的名字都以_结尾。

    一般来说,类的数据成员应该被定义为私有(private),这是面向对象编程中的封装原则。通过将数据成员设为私有,可以防止外部代码直接访问或修改这些数据,从而保护类的内部状态的完整性。

    然而,有时候,你可能会选择将某些数据成员设为公有(public)。这通常在数据成员是类的公开接口的一部分,或者类本身就是一个简单的数据结构时发生。

    至于是否在公有数据成员的名称后加_,这完全取决于你的命名约定。在某些命名约定中,可能会在所有数据成员的名称后加_,无论它们是公有的还是私有的。在其他命名约定中,可能只在私有数据成员的名称后加_

    总的来说,关键是选择一种命名约定,并在整个代码库中一致地遵循它,以提高代码的可读性和一致性。

    变量名称前加下划线:这通常用于表示私有成员变量或者类的内部变量。然而,根据C++标准,名称以一个下划线开头的变量可能被保留给编译器的实现,所以一般不推荐这种做法。

  6. 在编程中,ijk通常用作循环变量,特别是在嵌套循环中。idxjdx是对这些传统变量名的扩展,其中idx可能表示"index",jdx可能表示第二个索引。

    然而,更具描述性的变量名可能会使代码更易于理解。例如,如果idx遍历的是当前帧的目标检测框,那么你可能会选择名为curBoxIdx的变量名。同样,如果jdx遍历的是前一帧的目标检测框,那么你可能会选择名为prevBoxIdx的变量名。

  7. 在我们的系统中,所有环境变量都使用大写字母命名。所以当我们声明局部变量时,应该使用小写字母来声明,以避免环境和局部变量名发生冲突。

  8. 在命名变量时,使用后缀dirpath有助于区分变量的用途:

    • dir:通常用于表示一个目录(文件夹)。例如,dataset_dir表示数据集所在的目录。
    • path:通常用于表示一个具体的文件路径或目录路径。它可以是一个文件的完整路径,也可以是一个目录的路径。例如,dataset_path表示数据集的完整路径。
  9. 使用动词来命名函数是一种常见的做法。例如,我们不命名我们的函数:dateFormatting,我们将其命名为:formatDate

  10. 等等。

头文件

C++编程中常用的头文件

C++编程中常用的头文件包括:

  • 输入输出流
    • <iostream>:标准输入输出流
    • <fstream>:文件输入输出流
  • 标准库容器
    • <vector>:动态数组
    • <array>:固定大小数组
    • <list>:双向链表
    • <deque>:双端队列
    • <map>:关联容器(键值对)
    • <set>:集合
    • <unordered_map>:无序关联容器
    • <unordered_set>:无序集合
  • 字符串处理
    • <string>:字符串类
  • 算法
    • <algorithm>:常用算法(如排序、查找)
    • <numeric>:数值操作(如累加、乘积)
  • 多线程
    • <thread>:线程相关功能
    • <mutex>:互斥锁
    • <condition_variable>:条件变量
  • 智能指针
    • <memory>:智能指针(如std::unique_ptrstd::shared_ptr
  • 其他工具
    • <utility>:实用工具(如std::pairstd::move
    • <functional>:函数对象和绑定器
    • <chrono>:时间库
    • <limits>:数值极限
    • <cmath>:数学函数
    • <cstdlib>:通用工具(如动态内存管理、随机数生成)

这些头文件涵盖了C++编程的许多常见需求和功能。

其它头文件

与整数类型限制相关的常量

除了INT_MAX与INT32_MAX,常用的还有哪些?

常用的与整数类型限制相关的常量包括:

  • <climits>头文件
    • INT_MINint类型的最小值
    • UINT_MAXunsigned int类型的最大值
    • LONG_MINlong类型的最小值
    • LONG_MAXlong类型的最大值
    • ULONG_MAXunsigned long类型的最大值
    • LLONG_MINlong long类型的最小值
    • LLONG_MAXlong long类型的最大值
    • ULLONG_MAXunsigned long long类型的最大值
  • <cstdint>头文件
    • INT8_MIN, INT8_MAXint8_t类型的最小值和最大值
    • UINT8_MAXuint8_t类型的最大值
    • INT16_MIN, INT16_MAXint16_t类型的最小值和最大值
    • UINT16_MAXuint16_t类型的最大值
    • INT32_MIN, INT32_MAXint32_t类型的最小值和最大值
    • UINT32_MAXuint32_t类型的最大值
    • INT64_MIN, INT64_MAXint64_t类型的最小值和最大值
    • UINT64_MAXuint64_t类型的最大值

命名空间

是否使用命名空间?

在C++中, using namespace std; 这条语句的作用是将 std 命名空间中的所有名称导入到当前作用域中,使得我们可以直接使用 std 命名空间中的类型和函数,而无需每次都完整地书写它们的命名空间。

然而,这种做法也有其缺点。首先,它会导致命名空间污染,即同一个作用域中可能存在多个同名的类型或函数,导致编译器无法区分它们。其次,这种做法可能会导致程序效率降低,因为导入的命名空间中的类型和函数可能会增加编译时间和运行时间。

因此,有些开发者会选择显式地书写类型和函数的完整名称,例如 std::cout ,而不是使用 using namespace std; 。这种做法可以避免命名空间污染,并且可以确保编译器能够准确地解析类型和函数的名称。同时,这种做法也可以提高程序的可读性,使得代码更加清晰易懂。

总的来说,是否使用 using namespace std; ,以及是否显式地书写类型和函数的完整名称,取决于开发者的个人喜好和编程习惯。但是,在编写大型项目时,为了避免命名空间污染和保证程序的效率,建议尽量少使用 using namespace std; ,而是显式地书写类型和函数的完整名称。

常用的属于std的成员

C++标准库中常用的成员包括:

  • 容器类
    • std::vector
    • std::list
    • std::deque
    • std::array
    • std::set
    • std::map
    • std::unordered_set
    • std::unordered_map
  • 字符串类
    • std::string
    • std::wstring
  • 输入输出流
    • std::cin
    • std::cout
    • std::cerr
    • std::ifstream
    • std::ofstream
  • 智能指针
    • std::unique_ptr
    • std::shared_ptr
    • std::weak_ptr
  • 算法
    • std::sort
    • std::find
    • std::accumulate
    • std::transform
    • std::copy
  • 多线程
    • std::thread
    • std::mutex
    • std::lock_guard
    • std::unique_lock
    • std::condition_variable

这些是C++标准库中常用的一些成员,可以帮助你进行各种数据操作、输入输出处理、多线程编程等。

编写更简洁的代码

10 Practices I Try to Follow for Cleaner Code

Become a Better Coder: 10 Tips

Write Clean Functions - I Will Show You How

  1. 使用有意义的名字。像transactionHistory这样的变量或者像这样的方法 validateSufficientBalanceForRefund 清楚地传达他们的目的。

  2. 遵循单一职责原则(SRP)。通过将不同的职责委托给不同的类,每个类都有一个更改的理由,从而简化了维护并提高了可读性和可测试性。

  3. 缩短方法。函数应该只具有比其名称低一级的代码。将长方法分解为更小、更集中的方法可以使代码更不容易出错并且更易于维护。它还简化了调试并提高了清晰度。样式指南通常建议将方法保持在 20-30 行左右。如果某个方法超出了此范围,则通常表明该算法过于复杂或该方法试图执行的操作过多。

    1. 函数应该是可重用的。而且函数越大,可重用的可能性就越小。这也与为什么一个函数应该只做一件事相关。如果它只做一件事,那么它很可能会很小。即,将一个长函数分解为多个分管各个小功能的短函数组成。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    public static double calculateTotalRevenue(double[] prices, int[] quantities) {
    if (prices.length != quantities.length) {
    throw new IllegalArgumentException("Prices and quantities arrays must have the same length");
    }

    double totalRevenue = 0.0;

    for (int i = 0; i < prices.length; i++) {
    double price = prices[i];
    int quantity = quantities[i];
    double productRevenue = price * quantity;

    if (quantity >= 100) {
    productRevenue *= 0.85;
    } else if (quantity >= 50) {
    productRevenue *= 0.90;
    } else if (quantity >= 10) {
    productRevenue *= 0.95;
    }

    totalRevenue += productRevenue;
    }

    return totalRevenue;
    }

    让我们以一种每个方法只有一个抽象级别的方式增强我们的代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    public static double calculateTotalRevenue(double[] prices, int[] quantities) {
    if (prices.length != quantities.length) {
    throw new IllegalArgumentException("Prices and quantities arrays must have the same length");
    }

    double totalRevenue = 0.0;

    for (int i = 0; i < prices.length; i++) {
    double productRevenue = calculateProductRevenue(prices[i], quantities[i]);
    totalRevenue += productRevenue;
    }

    return totalRevenue;
    }

    private static double calculateProductRevenue(double price, int quantity) {
    double productRevenue = price * quantity;
    return applyQuantityDiscount(productRevenue, quantity);
    }

    private static double applyQuantityDiscount(double revenue, int quantity) {
    if (quantity >= 100) {
    return revenue * 0.85;
    } else if (quantity >= 50) {
    return revenue * 0.90;
    } else if (quantity >= 10) {
    return revenue * 0.95;
    }
    return revenue;
    }
  4. 以有意义的方式使用注释。过度使用注释或添加只是重申代码正在执行的操作的注释是多余的,并且不会增加任何价值。此外,过时或不正确的注释可能会误导开发人员并造成混乱。另外,如果逻辑已修改,请不要忘记更新注释。

  5. 一致的格式。一致的格式极大地提高了可读性和团队合作。有许多工具可以帮助制定缩进和间距的编码标准。另外,在任何 IDE 中,这都只是一个快捷方式。

  6. 提供有意义的报错消息。通过捕获特定的异常并提供有意义的错误消息,调试和理解错误变得更加容易。此外,使用记录器记录异常,而不是打印堆栈跟踪,而是将错误集成到集中式日志记录系统中,使它们更易于管理和监控。

  7. 让你的代码保持在界限内。将代码保持在边缘线内可以轻松快速扫描。 IDE 通常会提供指导原则,通常为每行 80 或 100 个字符(数量可自定义),以帮助遵循此实践。例如,IntelliJ IDEA 甚至提供了边距的可视化表示。此外,将长行分成更小的部分还可以促进更好的编码实践,例如将逻辑封装到命名良好的方法和类中。这简化了代码审查和协作,因为团队成员可以快速掌握代码的结构和意图,而无需排长队。

  8. 编写有意义的测试用例。有效的测试清晰、简洁,并专注于验证代码的特定行为,包括正常条件、边界情况和潜在错误。它们应该易于其他开发人员理解,明确正在测试的内容和原因。

  9. 审查你的代码。定期的代码审查对于确保质量、一致性和可维护性至关重要。代码审查对于知识共享和预先识别潜在问题的重要性怎么强调都不为过。永远不要懒惰这样做。更重要的是,始终对那些花时间审查和评论您的代码的人做出回应。确认他们的反馈,以表明他们的声音被听到并且他们的意见受到赞赏。这可以培养团队文化并加强关系。

  10. 不断改进你的方法。了解何时优先考虑清晰性而非简洁性、简单性而非复杂性以及特殊性而非通用性对于编写有效的代码和成为专业的团队成员至关重要。确保您的代码像您希望其他人的代码一样易于理解。

  11. 开闭原则 (OCP) 规定类、方法或函数必须对扩展开放,但不能对修改开放。这意味着定义的任何类、方法或函数都可以轻松地重用或扩展用于多个实例,而无需更改其代码。举个例子,我们有一个名为地址的类。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class Address:
    def __init__(self, country):
    self.country = country

    def get_capital(self):
    if self.country == 'canada':
    return "ottawa"
    if self.country == 'america':
    return "Washington D.C"
    if self.country == 'united Kingdom':
    return "London"

    address = Address('united Kingdom')
    print(address.get_capital())

    这不符合 OCP,因为每当有一个新的国家时,我们就需要编写一个新的if语句来补充它。现在这可能看起来很简单,但想象一下我们有 100 个或更多的国家/地区需要考虑。那看起来怎么样?这就是 OCP 发挥作用的地方。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    capitals = {
    'canada': "Ottawa",
    'america': "Washington D.C",
    'united Kingdom': "London"
    }

    class Address:
    def __init__(self, country):
    self.country = country

    def get_capital(self):
    return capitals.get(self.country, "Capital not found")

    address = Address('united Kingdom')
    print(address.get_capital())
  12. 避免使用幻数(Magic Numbers);避免对文件路径或 URL 进行硬编码;请改用配置文件或环境变量。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # good
    NUM_OF_ORDERS = 50
    SELECT TOP NUM_OF_ORDERS * FROM orders

    import os
    file_path = os.getenv("FILE_PATH")

    # bad
    SELECT TOP 50 * FROM orders

    file_path = "/path/to/file.txt"
  13. 避免深层嵌套。限制循环、条件或函数内的嵌套级别以提高可读性。

  14. 函数的理想参数数量为零。然后我们有一个和两个参数函数。应避免构造具有三个参数的函数。超过三个参数需要特殊的理由——即使这样,也不应该使用这样的函数。如果您有超过 3 个参数,将它们分组到一个对象中可能是一个解决方案。

代码注释

一般注释

C++

1
2
3
4
5
6
7
// 一般注释

/**
* @brief 多行注释
* @param
* @return
*/

python

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 单行注释

'''
使用 3 个单引号分别作为注释的开头和结尾
可以一次性注释多行内容
这里面的内容全部是注释内容
'''

"""
使用 3 个双引号分别作为注释的开头和结尾
可以一次性注释多行内容
这里面的内容全部是注释内容
"""

函数注释

C/C++注释规范

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* @brief 打开文件 \n
* 文件打开成功后,必须使用::CloseFile函数关闭
* @param[in] fileName 文件名
* @param[in] fileMode 文件模式,可以由以下几个模块组合而成:
* -r读取
* -w 可写
* -a 添加
* -t 文本模式(不能与b联用)
* -b 二进制模式(不能与t联用)
* @return 返回文件编号
* --1表示打开文件失败(生成时:.-1)
* @note文件打开成功后,必须使用::CloseFile函数关闭
* @par 示例:
* @code
* //用文本只读方式打开文件
* int ret = OpenFile("test.txt", "a");
* @endcode
* @see 指定参考信息。函数::ReadFile::CloseFile (“::”是指定有连接功能,可以看文档里的CloseFile变成绿,点击它可以跳转到CloseFile.)
* @deprecated 由于特殊的原因,这个函数可能会在将来的版本中取消
* @bug 调试Bug说明
* @warning 警告说明 (warning) 定义一些关于这个函数必须知道的事情
* @remarks 备注说明 (remarks) 定义一些关于这个函数的备注信息
* @todo 将要完成的工作 (todo) 说明哪些事情将在不久以后完成
* @example 使用例子说明 (example) 例子说明
*/
int OpenFile(const char* fileName, const char* fileMode);

/**
* @brief 关闭文件
* @param [in] file 文件
*
* @retval 返回值 0 成功
* @retval -1 失败
* @pre file 必须使用OpenFile的返回值
*/
int CloseFile(int file);

特殊注释

这些是注释中的标签(tag),有时也被称作“代码标签(codetag)”或“标记(token)”。

标识:

  • TODO:标记代码中需要实现的功能或任务。
  • FIXME:标记代码中需要修复的问题或缺陷。
  • XXX:如果代码中有该标识,说明标识处代码虽然实现了功能,但是实现的方法有待商榷,代码有问题或具误导性,需引起警惕。希望将来能改进,要改进的地方会在说明中简略说明。
  • HACK/BODGE/KLUDGE:标记临时性修复或不优雅的解决方案。英语翻译为砍。如果代码中有该标识,说明标识处代码我们需要根据自己的需求去调整程序代码。
  • BUG/DEBUG:标记已知的Bug或错误。
  • UNDONE:对之前代码改动的撤销。
  • NOTE:提供额外的注释或提示信息,帮助理解代码意图或设计决策。

格式:

1
2
3
4
5
6
7
/*
* 1. 使用大写字母
* 2. 只用双正斜杠//,而不是三个正斜杠 ///
* 3. 在标签后使用半角冒号 :
*/
//TODO: Need implementation.
//FIXME: We need to avoid the problem of duplicating windows when clicking multiple times on this menu item.

C++数据类型

C++形参和实参

参考资料

形参和实参是函数中的两个重要概念。

形参(形式参数)是在函数定义中出现的参数,它是一个虚拟参数,只有在函数调用时才会接收到传递进来的实际参数。形参可以被看作是一个占位符,在函数定义时并没有实际的数值,只有在函数调用时才会得到实参的数值。形参的主要作用是表示函数需要从外部传递进来的数据。

实参(实际参数)是在函数中实际出现的参数,它的值可以是常量、变量、表达式、类等。实参的值是确定的,必须在函数调用时提供。实参的主要作用是向函数传递数据,将数据的值传递给形参,在函数体内被使用。

要注意的是,形参和实参之间的传递方式有两种:值传递和地址传递。值传递是指将实参的值复制给形参,形参在函数内部使用时不会改变实参的值。而地址传递是指将实参的地址传递给形参,形参在函数内部使用时可以通过地址修改实参的值。

总结起来,形参是函数定义中的参数,是一个虚拟的占位符,用于接收函数调用时传递进来的实参。实参是函数调用时提供的具体数值,用于向函数传递数据。形参和实参之间的传递方式可以是值传递或地址传递。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include<iostream>
void exchange1(int x,int y);
void exchange2(int &x,int &y);
void exchange3(int *x,int *y);
using namespace std;
int main()
{
int a = 7777777,b = 666666;
exchange1(a,b);
cout << a << endl << b << endl;
exchange2(a,b);
cout << a << endl << b << endl;
exchange3(&a,&b);
cout << a << endl << b << endl;
}

// exchange1中a和b没有交换的原因是:x和y是函数定义中的形参,代码中吧实参a,b分别赋值给了x,y这两个形参变量。代码中只是替换掉了x和y的值,实参a和b不受影响,交换失败。
void exchange1(int x,int y)
{
int temp;
temp = y;
y = x;
x = temp;
}

// exchange2中,x和y是a和b的引用,操作x和y交换就等于a和b交换,交换成功。
void exchange2(int &x,int &y)
{
int temp;
temp = y;
y = x;
x = temp;
}

// exchange3中,x和y两个形参是a和b的指针,也就是存放实参的地址。然后函数里面交换了x和y指向的数据地址,也就是实参a和b,交换成功。
void exchange3(int *x,int *y)
{
int temp;
temp = *y;
*y = *x;
*x = temp;
}

C++修饰符类型

参考链接

C++ 允许在 char、int 和 double 数据类型前放置修饰符。

修饰符是用于改变变量类型的行为的关键字,它更能满足各种情境的需求。

下面列出了数据类型修饰符:

  • signed:表示变量可以存储负数。对于整型变量来说,signed 可以省略,因为整型变量默认为有符号类型。
  • unsigned:表示变量不能存储负数。对于整型变量来说,unsigned 可以将变量范围扩大一倍。
  • short:表示变量的范围比 int 更小。short int 可以缩写为 short。
  • long:表示变量的范围比 int 更大。long int 可以缩写为 long。
  • long long:表示变量的范围比 long 更大。C++11 中新增的数据类型修饰符。
  • float:表示单精度浮点数。
  • double:表示双精度浮点数。
  • bool:表示布尔类型,只有 true 和 false 两个值。
  • char:表示字符类型。
  • wchar_t:表示宽字符类型,可以存储 Unicode 字符。

修饰符 signed、unsigned、long 和 short 可应用于整型,signedunsigned 可应用于字符型,long 可应用于双精度型。

这些修饰符也可以组合使用,修饰符 signedunsigned 也可以作为 longshort 修饰符的前缀。例如:unsigned long int

C++ 允许使用速记符号来声明无符号短整数无符号长整数。您可以不写 int,只写单词 unsigned、shortlongint 是隐含的。

C++中的类型限定符

参考链接

类型限定符提供了变量的额外信息,用于在定义变量或函数时改变它们的默认行为的关键字。

限定符 含义
const const 定义常量,表示该变量的值不能被修改。
volatile 修饰符 volatile 告诉该变量的值可能会被程序以外的因素改变,如硬件或其他线程。。
restrict restrict 修饰的指针是唯一一种访问它所指向的对象的方式。只有 C99 增加了新的类型限定符 restrict。
mutable 表示类中的成员变量可以在 const 成员函数中被修改。
static 用于定义静态变量,表示该变量的作用域仅限于当前文件或当前函数内,不会被其他文件或函数访问。
register 用于定义寄存器变量,表示该变量被频繁使用,可以存储在CPU的寄存器中,以提高程序的运行效率。

逻辑运算符

在C++中,"或"逻辑可以使用逻辑运算符||来表示,|是按位或运算符。

"与"逻辑可以使用逻辑运算符&&来表示,&是按位与运算符。

在C++中,if ( IMU_VERSION == 0)if ( IMU_VERSION = 0)有着本质的区别。

if ( IMU_VERSION == 0)是一个比较操作,它检查IMU_VERSION是否等于0。如果IMU_VERSION的值为0,那么条件为真,if语句中的代码块将被执行。如果IMU_VERSION的值不为0,那么条件为假,if语句中的代码块将不会被执行。

if ( IMU_VERSION = 0)是一个赋值操作。它将IMU_VERSION的值设置为0,然后检查赋值后的IMU_VERSION是否为非零。在这种情况下,由于赋值为0,条件始终为假,因此if语句中的代码块将不会被执行。同时,这也改变了IMU_VERSION的原始值,这可能不是你想要的结果。因此,你应该谨慎使用赋值操作符=if语句中,除非你确实想在检查条件的同时赋值。

C++函数

普通函数、类的普通成员函数和类的静态成员函数之间的区别

在C++中,普通函数、类的普通成员函数和类的静态成员函数之间有以下几点主要区别:

  1. 普通函数:属于全局函数,不受具体类和对象的限制,可以直接调用。
    • 普通函数不属于任何类,它只能访问全局变量和其参数。它不能访问类的成员变量和成员函数(除非有一个类的对象或指针作为参数传入)。
  2. 类的普通成员函数:类的普通成员函数属于类的实例(对象),它可以访问类的所有成员(包括私有成员、保护成员和公有成员)。每个对象都有自己的成员函数副本。普通成员函数必须通过对象来调用。
    • 本质上是一个包含指向具体对象this指针的普通函数,即C++类的普通成员函数都隐式包含一个指向当前对象的this指针。
  3. 类的静态成员函数:类的静态成员函数属于类本身,而不属于类的任何对象。它只能访问类的静态成员变量和静态成员函数,不能访问类的非静态成员变量和非静态成员函数。静态成员函数可以通过类名直接调用,也可以通过对象调用。
    • 静态成员函数在某种程度上类似于全局函数,因为它们不依赖于类的任何特定对象,而是属于类本身。这意味着你可以在不创建类的对象的情况下调用静态成员函数。
    • 然而,静态成员函数并不完全等同于全局函数。静态成员函数仍然是类的一部分,它可以访问类的静态成员(包括私有静态成员),而全局函数则不能。
    • 静态成员函数没有this指针。this指针是一个指向调用成员函数的特定对象的指针。因为静态成员函数不依赖于任何特定对象,所以它没有this指针。这也意味着静态成员函数不能访问类的非静态成员变量或非静态成员函数。

如果成员函数想作为回调函数来使用,如创建线程等,一般只能将它定义为静态成员函数才行。

在C++中,回调函数通常需要是全局函数或静态成员函数,因为它们具有固定的函数签名,可以被用作函数指针。非静态成员函数不能直接用作回调函数,因为它们有一个隐含的this参数,这会改变它们的函数签名。

然而,有一些方法可以让你使用非静态成员函数作为回调函数。例如,你可以使用std::bindlambda表达式来捕获this指针,然后调用非静态成员函数。这种方法在C++11及以后的版本中可用。

以下是一个简单的例子来说明这三种函数的区别:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class MyClass {
public:
int x; // 普通成员变量
static int y; // 静态成员变量。只被初始化一次,下次执行初始化语句会直接跳过。

void func() { // 普通成员函数
x = 1; // 可以访问普通成员变量
y = 2; // 可以访问静态成员变量
}

static void staticFunc() { // 静态成员函数
// x = 3; // 错误:不能访问普通成员变量
y = 4; // 可以访问静态成员变量
}
};

int MyClass::y = 0; // 初始化静态成员变量

void globalFunc(MyClass& obj) { // 普通函数
// obj.x = 5; // 可以通过对象访问其成员变量
// MyClass::y = 6; // 可以访问静态成员变量
}

类的构造函数和析构函数的调用时机

在C++中,类的构造函数和析构函数的调用时机如下:

  1. 构造函数:当创建类的对象时,会自动调用类的构造函数。构造函数的主要任务是初始化对象的数据成员。构造函数的名称与类名相同,没有返回类型。构造函数可以有参数,可以被重载。

    • 当创建一个类的对象时,例如MyClass obj;,会调用默认构造函数(无参数的构造函数)。
    • 当以参数方式创建一个类的对象时,例如MyClass obj(1, 2);,会调用相应的参数构造函数。
    • 当一个对象作为另一个对象的初始化参数时,例如MyClass obj1 = obj2;MyClass obj1(obj2);,会调用拷贝构造函数。
  2. 析构函数:在C++中,当类的对象需要在其生命周期结束时执行某些操作(如释放资源、关闭文件、断开网络连接等)时,就需要定义析构函数。当一个对象的生命周期结束时(例如,对象离开其作用域,或者使用delete删除动态分配的对象),会自动调用其析构函数。析构函数的主要任务是执行清理工作,例如释放对象可能拥有的资源。析构函数的名称是类名前加上波浪号~,没有返回类型,也不能有任何参数,因此不能被重载。

    • 一个局部变量,那么当它的定义域(例如一个函数或一个代码块)结束时,它的析构函数会被调用。
    • 一个全局变量或者静态变量,那么当程序结束时,它的析构函数会被调用。

    而有的对象不会被析构函数自动释放,因此就不能使用空的析构函数,而是在析构函数内定义释放规则。

    以下是一个例子,该例子中的类FileWrapper封装了一个文件指针,需要在析构函数中关闭文件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    #include <cstdio>

    class FileWrapper {
    private:
    FILE* file_;

    public:
    FileWrapper(const char* filename, const char* mode) {
    file_ = std::fopen(filename, mode);
    if (file_ == nullptr) {
    throw std::runtime_error("Failed to open file.");
    }
    }

    ~FileWrapper() {
    if (file_ != nullptr) {
    std::fclose(file_); // 关闭文件
    }
    }

    // 其他成员函数...
    };

    在这个例子中,FileWrapper的构造函数打开一个文件,并将文件指针存储在file_中。然后,FileWrapper的析构函数在file_不为nullptr时关闭文件。这样,无论FileWrapper对象何时被销毁(例如,离开作用域或被delete删除),都会自动关闭文件,防止资源泄漏。

    如果FileWrapper使用空的析构函数,那么文件将不会被关闭,可能会导致资源泄漏和其他问题。

以下是一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MyClass {
public:
MyClass() {
// 构造函数
std::cout << "Constructor called!" << std::endl;
}

~MyClass() {
// 析构函数
std::cout << "Destructor called!" << std::endl;
}
};

int main() {
MyClass obj; // 创建对象,调用构造函数
// 当obj离开作用域时,调用析构函数
return 0;
}

在这个例子中,当MyClass obj;执行时,会调用MyClass的构造函数。当obj离开其作用域(即main函数结束时),会调用MyClass的析构函数。

输出函数的选择

  1. C++ 中 printfcout 有什么区别?

    在 C++ 中,printfcout 都用于输出,但它们来自不同的库,有不同的用法和特性。下面是一些主要的不同之处:

    1. 来源
      • printf 是 C 语言中的标准输入输出函数,来源于 C 语言的标准库(stdio.h)。在 C++ 中依然可用,但需要包含头文件 <cstdio> 或者 C 风格的 <stdio.h>
      • cout 是 C++ 专有的,属于 C++ 标准库中的一个对象,用于输出。cout 是定义在 <iostream> 头文件中的标准输出流对象。
    2. 使用语法
      • printf 使用格式化字符串。例如:printf("%d %s", 100, "C++");
      • cout 使用流插入运算符(<<)。例如:cout << 100 << " C++";
    3. 类型安全
      • printf 在处理类型时不够安全,因为它依赖于格式化字符串正确地匹配变量的类型;如果不匹配,可能会造成运行时错误或未定义行为。
      • cout 是类型安全的,编译器会检查插入到cout中的数据类型,确保与预期匹配,减少了错误的可能性。
    4. 性能
      • printf 的性能通常情况下比 cout 快,尤其是在格式化字符串操作时,因为 cout 的类型安全和重载运算符使得它的运行效率稍低。
      • 但是,通过启用 C++ 的 std::ios_base::sync_with_stdio(false); 以及避开使用 std::endlstd::endl 会刷新输出缓冲区),可以提升 cout 的性能,使其与 printf 相近。
    5. 灵活性和功能
      • 使用 cout 更灵活,因为它支持对输出格式的更细致控制,例如设置宽度、精度和填充字符等。此外,cout 可以直接打印用户定义的类型,只要为这些类型重载了输出运算符(<<)。
      • printf 在打印自定义类型时没有直接的方式,因为它需要预定义的格式化串。
    6. 可移植性
      • printf 基于 C 语言,因此在许多不同的平台和编译器中使用广泛,具有很高的可移植性。
      • cout 虽然是 C++ 的一部分,理论上在所有支持 C++ 的平台上都是可用的,但在跨平台编程时,对输出格式的控制可能会遇到不一致性。

    综上所述,printfcout 都有各自的用途和优势,选择使用哪个取决于具体的编程需求和个人喜好。在实际编程中,了解它们的差异有助于更好地决策和优化代码。

  2. 在C++程序开发中,推荐使用printf还是cout

    在 C++ 程序开发中,通常推荐使用 cout 而不是 printf。这一推荐基于以下几个理由:

    1. 类型安全cout 是类型安全的,这意味着编译器会对插入到 cout 的数据类型进行检查,减少了类型不匹配的错误。而 printf 需要开发者手动指定类型,在类型不匹配的情况下可能导致未定义行为或运行时错误。
    2. 易于使用:使用 cout 需要较少的手工干预来处理不同类型的数据,因为它可以自动调整输出格式,而 printf 请求用户显式地指定格式化字符串。特别是在处理字符串和数字的混合输出时,cout 的语法更为直观和简洁。
    3. 可扩展性:对于自定义类型,cout 允许开发者通过重载输出运算符 (<<) 来定义自己的输出格式,这提供了良好的可扩展性。而 printf 不能直接支持自定义类型的打印,除非通过某种方式将其转换为内建类型。
    4. 标准一致性cout 是 C++ 标准库的一部分,充分利用 cout 能够使程序更加面向对象,更符合 C++ 的编程风格。printf 来自 C 语言标准库,虽然在 C++ 中仍然可用,但并不是 C++ 风格的首选。
    5. 灵活性cout 提供了更好的控制输出格式的功能,如设置宽度、填充、精度等,而且这些设置易于读写和维护。

    然而,在某些特定情况下,printf 可能更为有用: - 当您需要更详细地控制输出格式,尤其是在涉及复杂格式时,printf 的格式化字符串可能更易于使用和理解。 - 在性能极致敏感的场景中,经过适当的优化后,printf 在某些情况下可能比 cout 有更好的性能。

    总之,虽然 cout 通常是在 C++ 中处理输出的首选,但确切选择取决于具体的应用场景、性能需求和个人偏好。在决定使用哪一个之前,考虑项目的需求和上下文非常重要。

  3. 在使用C++语言的ROS平台开发中,推荐使用printfcout还是ROS_INFO

    在使用 C++ 语言开发 ROS (Robot Operating System) 平台的应用时,通常推荐使用 ROS_INFO 以及其相关的宏(如 ROS_WARN, ROS_ERROR, ROS_DEBUG 等)来进行消息的输出。这些宏专为 ROS 开发设计,提供了一些优势和特性,特别是在进行机器人软件开发时:

    1. 统一的日志管理ROS_INFO 和其他相关宏能够将输出消息统一管理,使得日志记录、查看和分析变得更加容易。ROS 提供的 rqt_console 和 roslaunch 日志文件都可以捕捉这些输出,方便开发者调试和追踪问题。
    2. 级别管理:不同于 printfcout 只能单纯输出文本,ROS_INFO 等宏允许你按照不同的严重性级别输出消息,如信息、警告、错误、调试等。这有助于开发者在运行时筛选和控制想要查看的输出类型。
    3. 节点识别:使用 ROS 日志函数,每条日志消息都会自动附带节点名信息。这对于在复杂系统中跟踪消息来源特别有用,尤其是当多个节点同时运行时。
    4. 格式化和类型安全ROS_INFO 等宏支持类似 printf 的格式化功能,使得在输出文本时可以包含变量。同时,由于 ROS_INFO 是在 C++ 环境下设计的,因此也可以在一定程度上享受到 C++ 类型安全的好处(尽管其内部实现可能是利用 printf 风格的格式化字符串)。
    5. 条件编译ROS_DEBUG 等调试相关的宏,在发布模式下可以通过编译选项完全禁用,从而不会对发布版本的性能造成影响。

    综上所述,虽然在 ROS 平台下开发时 printfcout 依然可用,但 ROS_INFO 和相关的宏提供了更多符合 ROS 开发环境和需求的优势。因此,在大多数情况下,推荐使用 ROS_INFO 及其相关宏进行消息输出和日志记录。

  4. 在使用C++语言的ROS平台开发中,推荐使用printfcoutROS_INFO还是ROS_INFO_STREAM

    在使用C++语言的ROS(Robot Operating System)平台开发时,选择printfcoutROS_INFO还是ROS_INFO_STREAM取决于具体的应用场景和你的偏好。下面是它们各自的特点和适用情况:

    printf

    • 来源于C语言,C++也支持。
    • 用于格式化输出,语法稍显复杂,对类型安全的支持不如C++的IO流。
    • 适用于快速的调试或者当开发环境更接近C语言时使用。

    std::cout

    • C++标准输入输出流的一部分,使用起来比printf更为方便和安全。
    • 支持类型安全的输出,可以通过重载<<操作符来扩展对自定义类型的支持。
    • 适用于C++程序的通用输出,特别是需要输出自定义类型数据时。

    ROS_INFOROS_INFO_STREAM

    • 这是ROS中专门用于日志输出的宏和流。
    • ROS_INFO 类似于printf,而ROS_INFO_STREAM则类似于std::cout,使用方法根据个人偏好和输出内容的复杂度来选择。
    • 它们的优势在于:
    • 集成了ROS的命名空间和节点名,可以更清楚地知道是哪个节点产生的日志。
    • 可以通过ROS的配置文件来调整日志的级别,方便调试和运行时的日志管理。
    • 支持网络日志(rosout),使得可以在ROS的不同部分或不同机器上收集和查看日志。

    推荐使用

    • 对于简单的调试信息,如果你更习惯C++风格,推荐使用ROS_INFO_STREAM;如果你倾向于使用类似C语言的格式化输出,可以选择ROS_INFO
    • 对于非ROS系统级的调试或者涉及大量复杂数据类型输出时,std::cout可能更为直接和方便。
    • 一般建议尽量避免使用printf,除非你有特别的理由,因为它不提供类型安全且在C中使用std::coutROS_INFO_STREAM可以更好地利用C的特性。

    综上所述,选择哪一种取决于你的具体需求和开发习惯。在ROS开发中,ROS_INFOROS_INFO_STREAM因为其与ROS系统的集成,通常会是首选。

    ROS消息打印

C++小技巧

模板参数必须在编译时是已知的常量

在C++中,模板参数必须在编译时是已知的常量。这意味着你不能使用运行时从配置文件中读取的值作为模板参数。

一种可能的解决方案是使用预处理器宏来定义N_dim。你可以在编译时通过编译器的命令行参数来设置这个宏。例如:

1
2
3
#define N 512  // 默认值。
template <unsigned int N>
eFFT<N> efft; // 模板类

预处理器宏(如#define N 512)在它被定义的文件以及#include该文件的所有文件中都是可见的。如果你在一个文件中定义了N,然后在另一个文件中想要使用它,你需要确保第二个文件#include了定义N的文件。

然后在编译时,你可以使用-D选项来设置N_dim的值:

1
g++ -DN_dim=1024 -o my_program my_program.cpp

另一种解决方案是使用一个固定大小的eFFT对象,然后在运行时根据需要使用其中的一部分。这可能需要你修改eFFT类的实现,使其能够处理不同大小的输入。

重复定义问题

你的问题是在多个源文件中都包含了parameters.h,并且在这个头文件中定义了eFFT_SIZE。这导致了eFFT_SIZE的多重定义。

解决这个问题的一种方法是在parameters.h中只声明eFFT_SIZE,然后在一个源文件(例如parameters.cpp)中定义它。这样,eFFT_SIZE就只在一个地方定义了。

首先,你需要在parameters.h中将eFFT_SIZE的定义改为声明:

1
extern const unsigned int eFFT_SIZE;

然后,在parameters.cpp中定义eFFT_SIZE

1
const unsigned int eFFT_SIZE = 128;

这样,eFFT_SIZE就只在parameters.cpp中定义了一次,而在其他源文件中,它只是一个外部链接的声明。这应该可以解决你的问题。d

函数多个返回值

在C++中,有几种方法可以实现函数的多个返回值:

  1. 使用引用参数(Reference Parameters)/指针:你可以通过引用参数来修改函数外部的变量,从而实现多个返回值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    void getValues(int& x, int& y) {
    x = 5;
    y = 10;
    }

    int main() {
    int a, b;
    getValues(a, b);
    // Now a == 5 and b == 10
    }
  2. 使用std::pair或std::tuple:如果你的函数需要返回两个或更多的值,你可以使用std::pairstd::tuple

    1
    2
    3
    4
    5
    6
    7
    8
    std::pair<int, int> getValues() {
    return std::make_pair(5, 10);
    }

    int main() {
    std::pair<int, int> values = getValues();
    // Now values.first == 5 and values.second == 10
    }
  3. 使用结构体(Structs)或类(Classes):如果你的函数需要返回多个相关的值,你可以定义一个结构体或类来存储这些值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    struct Values {
    int x;
    int y;
    };

    Values getValues() {
    Values values;
    values.x = 5;
    values.y = 10;
    return values;
    }

    int main() {
    Values values = getValues();
    // Now values.x == 5 and values.y == 10
    }
  4. 使用数组。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    #include <stdio.h> 

    // 将较大的元素存储在arr[0]中
    void findGreaterSmaller(int a, int b, int arr[])
    {
    // Store the greater element at
    // 0th index of the array
    if (a > b) {
    arr[0] = a;
    arr[1] = b;
    }
    else {
    arr[0] = b;
    arr[1] = a;
    }
    }

    // Driver code
    int main()
    {
    int x, y;
    int arr[2];
    printf("输入两个数字: \n");
    scanf("%d%d", &x, &y);
    findGreaterSmaller(x, y, arr);
    printf("\n最大值为:%d,最小值为:%d",
    arr[0], arr[1]);
    return 0;
    }
  5. 选择哪种方法取决于你的具体需求和编程风格。

多个源文件中共享一个变量/全局变量

在C++中,如果你想在多个源文件中共享一个变量,你可以使用extern关键字。你可以在一个源文件中定义一个变量,并在其他源文件中使用extern关键字声明同一个变量。

首先,在ESVIO/pose_graph/src/pose_graph_node.cpp文件中,你需要将SYSTEM_MODE变量的定义移动到一个头文件中,例如global.h。然后在pose_graph_node.cppkeyframe.cpp中都包含这个头文件。

global.h文件内容如下:

1
2
3
4
5
6
#ifndef GLOBAL_H
#define GLOBAL_H

extern int SYSTEM_MODE;

#endif // GLOBAL_H

然后在ESVIO/pose_graph/src/pose_graph_node.cpp文件中,你需要包含global.h并定义SYSTEM_MODE

1
2
3
#include "global.h"

int SYSTEM_MODE = 0; // 初始化SYSTEM_MODE

最后,在ESVIO/pose_graph/src/keyframe.cpp文件中,你也需要包含global.h。这样你就可以在keyframe.cpp中使用SYSTEM_MODE变量了:

1
2
3
#include "global.h"

// 现在你可以在这个文件中使用SYSTEM_MODE变量

注意,extern关键字告诉编译器变量的定义在别的地方,你不能在声明的时候初始化它。变量的定义(也就是初始化)应该在一个源文件中进行,而不是头文件。如果你在多个地方定义了同一个extern变量,那么会导致链接错误。

程序计时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#pragma once

#include <ctime>
#include <cstdlib>
#include <chrono>

// TicToc类,记录时间
class TicToc
{
public:
TicToc()
{
tic();
}

void tic()
{
start = std::chrono::system_clock::now();
}

double toc()
{
end = std::chrono::system_clock::now();
std::chrono::duration<double> elapsed_seconds = end - start;
return elapsed_seconds.count() * 1000;
}

private:
std::chrono::time_point<std::chrono::system_clock> start, end;
};

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
#include "tic_toc.h"

void some_function_to_test()
{
// 这里放置你要测试的代码
for (int i = 0; i < 1000000; ++i)
{
// 模拟一些工作
}
}

int main()
{
const int num_iterations = 100; // 测试次数
double total_time = 0.0;

for (int i = 0; i < num_iterations; ++i)
{
TicToc timer;
some_function_to_test();
double elapsed_time = timer.toc();
total_time += elapsed_time;
}

double average_time = total_time / num_iterations;
std::cout << "Average execution time: " << average_time << " ms" << std::endl;

return 0;
}

类与对象

为什么要定义类与对象,什么时候定义类与对象?

在面向对象的编程语言中,类(Class)是一种定义新类型的方式,它封装了数据和操作数据的方法。对象(Object)则是类的实例,它包含了类定义的数据和方法。

定义类和对象的目的主要有以下几点:

  1. 封装:类可以将数据和操作数据的方法封装在一起,隐藏内部实现细节,提高代码的安全性和可维护性。
  2. 继承:类可以继承其他类的属性和方法,实现代码的复用。
  3. 多态:通过接口或者虚函数,不同的类对象可以执行相同的操作,但表现出不同的行为,提高代码的灵活性。

何时定义类和对象主要取决于你的需求。以下是一些可能需要定义类和对象的情况:

  1. 当你需要创建一个复杂的数据结构,这个数据结构包含多个相关的数据和操作这些数据的方法时,你可以定义一个类来封装这些数据和方法。
  2. 当你需要创建多个具有相同属性和行为的对象时,你可以定义一个类,然后创建这个类的多个对象。
  3. 当你需要实现面向对象的设计模式(如工厂模式、单例模式等)时,你需要定义类和对象。

怎么用类与对象

尽量实现,提供类所需要的输入数据,所有操作在类内部封装和实现,在外部直接输出结果。

这个想法是面向对象编程(OOP)的核心原则之一,称为封装(Encapsulation)。封装是将数据(变量)和操作数据的函数(方法)捆绑在一起形成一个对象(即类的实例),并规定了外部对这些数据和函数的访问方式。

封装有以下几个优点:

  1. 提高安全性:通过隐藏对象的内部状态,只允许通过对象的方法来访问和修改,可以防止外部代码随意修改对象的内部状态,提高了代码的安全性。
  2. 简化接口:对象的用户只需要知道对象提供了哪些方法,而不需要知道这些方法是如何实现的。这使得代码更易于理解和使用。
  3. 提高可维护性:由于对象的内部实现被封装起来,所以在不影响对象的用户的情况下,可以更容易地改变对象的内部实现。

面向对象编程(OOP)的核心原则主要有以下四个:

  1. 封装(Encapsulation):封装是将对象的状态(属性)和行为(方法)捆绑在一起,同时隐藏对象的内部实现细节,只提供有限的接口供外部访问。这样可以保护对象的内部状态,提高代码的安全性和可维护性。
  2. 继承(Inheritance):继承是子类可以继承父类的属性和方法,实现代码的复用。子类可以扩展和修改父类的行为,提供更具体的功能。
  3. 多态(Polymorphism):多态是指不同的对象可以对同一消息做出不同的响应。在运行时,根据对象的实际类型来调用相应的方法,提高了代码的灵活性。
  4. 抽象(Abstraction):抽象是将复杂的系统简化为更简单的模型。通过定义抽象的类和接口,隐藏具体的实现细节,让程序员只关注有用的信息。

这四个原则是面向对象编程的基础,它们使得代码更易于理解、维护和扩展。

模板类

模板类是C++中一种特殊的类,它可以用于创建处理不同数据类型的类的蓝图。模板类的定义以关键字template开始,后面跟一个或多个模板参数。

例如,你可以定义一个模板类Array,它可以用于创建处理不同类型元素的数组:

1
2
3
4
5
template <typename T, int N>
class Array {
T elements[N];
// ...
};

在这个例子中,T是一个类型模板参数,N是一个非类型模板参数。你可以用任何类型替换T,用任何整数替换N,来创建不同的Array类:

1
2
Array<int, 10> intArray;
Array<double, 20> doubleArray;

模板类与一般的类的主要区别在于,模板类可以处理多种类型的数据,而一般的类只能处理特定类型的数据。模板类提供了一种机制,使得你可以在类定义时不指定具体的类型,而是在使用类时指定类型。这使得你的代码更加灵活,可以处理多种类型的数据。

成员初始化列表

成员初始化列表(Member Initializer List)是C++中一个重要的特性,它允许在构造函数中初始化类的成员变量。这个特性不仅可以提高代码的清晰度和执行效率,还是某些情况下初始化成员变量的唯一方法。

在C++中,构造函数可以包含一个初始化列表,用于在进入构造函数体之前初始化成员变量。初始化列表以冒号(:)开头,后面跟着用逗号(,)分隔的初始化表达式。例如:

1
2
3
4
5
6
7
8
class Example {
public:
// 含有两个参数的构造函数
Example(int x, int y) : x_(x), y_(y) {} // 用接收到的 int 类型的变量 x 的值来初始化 x_ 。
private:
int x_;
int y_;
};

在这个例子中,*x_y_*是通过成员初始化列表进行初始化的,而不是在构造函数体内赋值。

有几种情况下必须使用成员初始化列表:

  1. const成员变量:const成员变量必须在声明时初始化,不能在构造函数体内赋值。
  2. 引用成员变量:引用成员同样需要在声明时初始化。
  3. 没有默认构造函数的类类型成员:如果类成员本身是一个对象,并且这个对象没有无参的构造函数,那么必须通过成员初始化列表来调用其有参构造函数。
  4. 继承中的基类成员:如果需要在子类中初始化基类的私有成员,也需要在成员初始化列表中显式调用基类的构造函数。

使用成员初始化列表初始化类成员比在构造函数体内赋值有几个优势:

  • 性能:对于类类型的成员变量,使用成员初始化列表可以避免调用默认构造函数后再赋值,减少了一次不必要的构造和析构过程,提高了效率。
  • 顺序:成员变量的初始化顺序与它们在类定义中的声明顺序一致,与初始化列表中的顺序无关。这有助于避免一些因初始化顺序不当导致的错误。
1
2
3
4
5
6
7
8
9
10
11
12
13
class A {
private:
const int a; // const成员
public:
A() : a(10) {} // 使用成员初始化列表进行初始化
};

class B {
private:
int &b; // 引用成员
public:
B(int &b_ref) : b(b_ref) {} // 使用成员初始化列表进行初始化
};

在这个示例中,类A的成员a是一个const成员,类B的成员b是一个引用成员。它们都通过成员初始化列表进行了初始化。

其它

  1. 在C++中,如果一个类没有明确指定访问修饰符(publicprotectedprivate),那么默认的访问级别是private。这意味着,如果你没有在类定义中看到任何访问修饰符,那么该类的所有成员都是私有的。

  2. 初始赋值:

    1
    2
    3
    4
    5
    class FeatureTracker
    {
    private:
    int trackIDCounter_ = 1;
    }

    trackIDCounter_ = 1;是对成员变量trackIDCounter_的初始赋值。这意味着,当创建这个类的对象时,trackIDCounter_的初始值将被设置为1。

    在类的成员函数中,你可以更改这个成员变量的值。例如,你可以在一个成员函数中增加trackIDCounter_的值:

    1
    2
    3
    void FeatureTracker::incrementTrackID() {
    trackIDCounter_++;
    }

    在这个例子中,incrementTrackID函数将trackIDCounter_的值增加1。你可以在类的任何成员函数中更改trackIDCounter_的值,只要这个函数有权限访问这个成员变量(即,这个函数不是const的)。

  3. 等等。

结构体

概述

定义一个结构体类型就类似于定义了一个变量类型,结构体的用法类似于变量,只不过一个结构体里可以包含多个变量类型。

结构体(struct)在C++中是一种复合数据类型,它可以包含多个不同类型的数据成员。你可以将结构体看作是一个“自定义的数据类型”,这个数据类型可以包含多个其他的数据类型。

例如,你可以定义一个BoundingBox结构体来表示一个目标检测框,这个结构体包含了xywh四个整型数据成员。然后,你就可以像使用其他数据类型一样使用这个BoundingBox结构体,例如创建BoundingBox类型的变量,将BoundingBox类型的变量作为函数的参数,等等。

将一组相关的变量定义为结构体(struct)有以下几个好处:

  1. 组织性:结构体可以将一组相关的数据组织在一起,使代码更加清晰和易于理解。

  2. 代码复用:你可以多次使用同一个结构体,这有助于减少代码重复。

    如果你需要在其他地方使用相同的一组变量,你可能需要再次声明和初始化这些变量,这就是代码重复。但是,如果你将这些变量定义为一个结构体,你就可以在需要的地方创建这个结构体的实例,而不需要重复声明和初始化这些变量。这就是代码复用。以下是一个例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    struct BoundingBox {
    int x;
    int y;
    int w;
    int h;
    };

    void processBoundingBox(BoundingBox box) {
    // 在这个函数中处理目标检测框
    }

    int main() {
    BoundingBox box1 = {1, 2, 3, 4};
    processBoundingBox(box1);

    BoundingBox box2 = {5, 6, 7, 8};
    processBoundingBox(box2);

    return 0;
    }

    在这个例子中,我们定义了一个BoundingBox结构体,并在processBoundingBox函数中使用它。在main函数中,我们创建了两个BoundingBox的实例box1box2,并将它们传递给processBoundingBox函数进行处理。这样,我们就复用了BoundingBox结构体的定义,而不需要在每个需要使用目标检测框的地方都声明和初始化xywh这四个变量。

  3. 易于维护:如果你需要修改这组数据,只需要在一个地方(即结构体定义)进行修改,而不是在代码的多个地方。

    当你需要修改或更新代码时,结构体可以使这个过程更加简单和直接。举个例子,假设你有一个关于目标检测框的结构体:

    1
    2
    3
    4
    5
    6
    struct BoundingBox {
    int x;
    int y;
    int w;
    int h;
    };

    现在,你想要添加一个新的属性,比如目标检测框的颜色。如果你没有使用结构体,你可能需要在代码的多个地方添加新的变量,并且需要确保这些变量在所有的函数和方法中都被正确地更新和使用。

    但是,如果你使用了结构体,你只需要在结构体的定义中添加新的属性:

    1
    2
    3
    4
    5
    6
    7
    struct BoundingBox {
    int x;
    int y;
    int w;
    int h;
    std::string color;
    };

    这样,所有使用BoundingBox的地方都会自动获得新的color属性,你只需要在适当的地方更新和使用这个新的属性即可。这就是结构体使代码"易于维护"的一个例子。

  4. 封装:结构体可以封装数据和操作,使得数据和操作紧密相关,提高代码的可读性和可维护性。

使用

定义

在C++中,struct可以包含各种类型的成员,包括基本类型(如intdouble等)、类对象、数组、vector等。以下是一个例子:

1
2
3
4
5
6
#include <vector>

struct MyStruct {
int id;
std::vector<int> values;
};

在这个例子中,MyStruct包含一个int类型的成员id和一个vector<int>类型的成员values

你可以定义一个结构体来表示单个目标检测框,然后使用vector来存储多个这样的目标检测框。这样做的好处是,你可以很容易地添加、删除和遍历目标检测框,而且代码的可读性和可维护性也会提高。

1
2
3
4
5
6
7
8
9
10
11
struct BoundingBox {
int class_id;
float class_confidence;
int x;
int y;
int w;
int h;
};

vector<BoundingBox> boxes_left;
vector<BoundingBox> boxes_right;

在这个例子中,BoundingBox是一个结构体,它表示一个目标检测框。boxes_leftboxes_right是两个vector,它们分别存储左目和右目的目标检测框。

当你需要添加一个新的目标检测框时,你可以创建一个BoundingBox的实例,设置它的属性,然后将它添加到boxes_leftboxes_right中。当你需要遍历所有的目标检测框时,你可以遍历boxes_leftboxes_right,并对每个BoundingBox实例进行操作。

赋值与引用

在C++中,你可以通过.操作符来引用struct中的成员。如果你的struct中有一个vector成员,你可以像下面这样引用它:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <vector>

struct MyStruct {
int id;
std::vector<int> values;
};

int main() {
MyStruct s;
s.values.push_back(1); // 向vector中添加一个元素
int firstValue = s.values[0]; // 访问vector中的第一个元素
return 0;
}

在这个例子中,MyStruct是一个结构体,它有一个vector<int>类型的成员values。在main函数中,我们创建了一个MyStruct类型的变量s,然后通过.操作符来访问和操作它的values成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
struct BoundingBox {
int class_id;
float class_confidence;
int x;
int y;
int w;
int h;
};

int main() {
vector<BoundingBox> boxes_left;

// 赋值
for (int i = 0; i < detection_left.detections.size(); i++) {
BoundingBox box;
box.x = detection_left.detections[i].x * scale;
box.y = detection_left.detections[i].y * scale;
box.w = detection_left.detections[i].w * scale;
box.h = detection_left.detections[i].h * scale;
box.class_id = detection_left.detections[i].class_id;
box.class_confidence = detection_left.detections[i].class_confidence;

boxes_left.push_back(box);
}

// 索引
for (int i = 0; i < boxes_left.size(); i++) {
int x = boxes_left[i].x;
int y = boxes_left[i].y;
int w = boxes_left[i].w;
int h = boxes_left[i].h;
int class_id = boxes_left[i].class_id;
float class_confidence = boxes_left[i].class_confidence;

// 在这里处理x, y, w, h, class_id, class_confidence
}
}

在这个例子中,我们首先创建了一个BoundingBox的实例box,然后设置了box的属性,最后将box添加到boxes_left中。这个过程在循环中重复,直到处理完所有的目标检测框。

我们使用了boxes_left[i]来访问boxes_left中的第i个元素,然后使用.操作符来访问这个元素的数据成员。这个过程在循环中重复,直到处理完boxes_left中的所有元素。

在C++中,你可以使用vectorclear方法来清空vector中的所有元素。以下是一个例子:

1
boxes_left.clear();

在这个例子中,boxes_left.clear()将清空boxes_left中的所有元素。这个操作将使boxes_left的大小变为0,但不会改变它的容量。如果你希望同时清空vector的元素和容量,你可以使用swap方法:

1
vector<BoundingBox>().swap(boxes_left);

在这个例子中,vector<BoundingBox>().swap(boxes_left)将创建一个新的空vector,然后与boxes_left交换。这个操作将使boxes_left的大小和容量都变为0。

清空/初始化

在C++中,你可以使用构造函数或者赋值运算符来初始化或清空一个结构体的值。以下是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
struct BoundingBox {
int x;
int y;
int w;
int h;

// 构造函数,用于初始化结构体的值
BoundingBox() : x(0), y(0), w(0), h(0) {}

// 一个方法,用于清空结构体的值
void clear() {
x = 0;
y = 0;
w = 0;
h = 0;
}
};

int main() {
BoundingBox box;

// 使用构造函数初始化结构体的值
box = BoundingBox();

// 使用方法清空结构体的值
box.clear();

return 0;
}

在这个例子中,BoundingBox结构体有一个构造函数,它将所有的数据成员初始化为0。clear方法将所有的数据成员设置为0。在main函数中,我们创建了一个BoundingBox类型的变量box,然后使用构造函数和clear方法来初始化和清空box的值。

作为函数的参数

在C++中,你可以将结构体作为函数的输入参数或输出参数。以下是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>

// 定义一个结构体
struct Point {
int x;
int y;
};

// 将结构体作为输入参数的函数
void printPoint(Point p) {
std::cout << "Point: (" << p.x << ", " << p.y << ")" << std::endl;
}

// 将结构体作为输出参数的函数
Point getPoint() {
Point p;
p.x = 10;
p.y = 20;
return p;
}

int main() {
Point p1 = {1, 2};
printPoint(p1); // 输出: Point: (1, 2)

Point p2 = getPoint();
printPoint(p2); // 输出: Point: (10, 20)

return 0;
}

在这个例子中,printPoint函数接收一个Point类型的参数,getPoint函数返回一个Point类型的值。在main函数中,我们创建了两个Point类型的变量p1p2,并使用printPoint函数打印它们的值。

定义结构体里的函数

https://www.runoob.com/cplusplus/cpp-struct.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <iostream>
#include <string>

using namespace std;

// 声明一个结构体类型 Books
struct Books
{
string title;
string author;
string subject;
int book_id;

// 构造函数
Books(string t, string a, string s, int id)
: title(t), author(a), subject(s), book_id(id) {}
};

// 打印书籍信息的函数
void printBookInfo(const Books& book) {
cout << "书籍标题: " << book.title << endl;
cout << "书籍作者: " << book.author << endl;
cout << "书籍类目: " << book.subject << endl;
cout << "书籍 ID: " << book.book_id << endl;
}

int main()
{
// 创建两本书的对象
Books Book1("C++ 教程", "Runoob", "编程语言", 12345);
Books Book2("CSS 教程", "Runoob", "前端技术", 12346);

// 输出书籍信息
printBookInfo(Book1);
printBookInfo(Book2);

return 0;
}

结构体与类的区别

在 C++ 中,struct 和 class 本质上非常相似,唯一的区别在于默认的访问权限:

  • struct 默认的成员和继承是 public
  • class 默认的成员和继承是 private

你可以将 struct 当作一种简化形式的 class,适合用于没有太多复杂功能的简单数据封装。

数组

定义

C++代码 int record[26] = {1}; 的作用是:

  1. 声明一个数组int record[26] 声明了一个包含26个整数的数组,数组元素类型为int
  2. 初始化数组= {1} 用于初始化数组。这里表示将数组的第一个元素初始化为1,其余的元素初始化为0。

具体来说,这行代码将创建一个名为record的整数数组,数组大小为26,并将数组的第一个元素(record[0])初始化为1,剩余的元素(record[1]record[25])初始化为0。

这种初始化方式的具体效果如下:

  • record[0] 被初始化为 1
  • record[1]record[25] 被初始化为 0

这种用法常见于需要部分已知初始值的数组,而其他部分可以安全地初始化为零。例如,如果你需要一个数组来记录某些变量的初始状态,其中只有第一个变量有特定的初始值,这种方式就很有用。

大小

在C++中,有几种常见的方法来获取数组的大小以便用于循环遍历:

使用sizeof运算符

如果你在使用静态数组(编译时知道大小的数组),你可以使用sizeof运算符来计算数组的大小。以下是一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>

int main() {
int arr[] = {1, 2, 3, 4, 5};
int size = sizeof(arr) / sizeof(arr[0]); // 计算数组的大小

for (int i = 0; i < size; ++i) {
std::cout << arr[i] << " ";
}

return 0;
}

在这个示例中,sizeof(arr)返回数组的总字节数,sizeof(arr[0])返回单个元素的字节数。通过将总字节数除以单个元素的字节数,我们就可以得到数组的大小。

使用std::array

如果你在使用C++11及以上版本,推荐使用std::array,它是一个封装了静态数组的模板类,并提供了一个可以直接获取大小的方法。以下是一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <array>

int main() {
std::array<int, 5> arr = {1, 2, 3, 4, 5};
int size = arr.size(); // 获取数组的大小

for (int i = 0; i < size; ++i) {
std::cout << arr[i] << " ";
}

return 0;
}

在这个示例中,arr.size()直接返回数组的大小,使用起来非常方便。

使用std::vector

如果你需要一个动态大小的数组,推荐使用std::vector,它是一个动态数组模板类。以下是一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <vector>

int main() {
std::vector<int> arr = {1, 2, 3, 4, 5};
int size = arr.size(); // 获取数组的大小

for (int i = 0; i < size; ++i) {
std::cout << arr[i] << " ";
}

return 0;
}

在这个示例中,arr.size()直接返回数组的大小,使用起来也非常方便。

总结

  • 对于静态数组,使用sizeof运算符。
  • 对于C++11及以上版本的静态数组,使用std::array
  • 对于动态数组,使用std::vector

选择合适的方法可以使代码更加简洁和易于维护。

set

定义与使用

C++ 容器类 | 菜鸟教程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <iostream>
#include <set>

int main() {
// 声明一个整型 set 容器
std::set<int> mySet;

// 插入元素
mySet.insert(10);
mySet.insert(20);
mySet.insert(30);
mySet.insert(40);

// 输出 set 中的元素
std::cout << "Set contains: ";
for (int num : mySet) {
std::cout << num << " ";
}
std::cout << std::endl;

// 查找元素
if (mySet.find(20) != mySet.end()) {
std::cout << "20 is in the set." << std::endl;
} else {
std::cout << "20 is not in the set." << std::endl;
}

// 删除元素
mySet.erase(20);

// 再次输出 set 中的元素
std::cout << "After erasing 20, set contains: ";
for (int num : mySet) {
std::cout << num << " ";
}
std::cout << std::endl;

// 检查 set 是否为空
if (mySet.empty()) {
std::cout << "The set is empty." << std::endl;
} else {
std::cout << "The set is not empty." << std::endl;
}

// 输出 set 中元素的数量
std::cout << "The set contains " << mySet.size() << " elements." << std::endl;

return 0;
}

举例

两个数组的交集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> result_set; // 存放结果,之所以用set是为了给结果集去重
unordered_set<int> nums_set(nums1.begin(), nums1.end());
for (int num : nums2) {
// 发现nums2的元素 在nums_set里又出现过
// find 是 unordered_set 类的一个成员函数,用于在集合中查找指定元素。
// 如果找到该元素,find 返回一个指向该元素的迭代器;
// 如果没有找到,则返回一个指向集合末尾的迭代器(即 end() 迭代器)。
if (nums_set.find(num) != nums_set.end()) {
result_set.insert(num);
}
}
return vector<int>(result_set.begin(), result_set.end());
}
};

map

定义和使用

C++ 容器类 | 菜鸟教程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>#include <map>
#include <string>

int main() {
// 创建一个 map 容器,存储员工的姓名和年龄
std::map<std::string, int> employees;

// 插入员工信息
employees["Alice"] = 30;
employees["Bob"] = 25;
employees["Charlie"] = 35;
// 另一种插入形式
map.insert(pair<int, int>(nums, i));

// 遍历 map 并打印员工信息
for (std::map<std::string, int>::iterator it = employees.begin(); it != employees.end(); ++it) {
std::cout << it->first << " is " << it->second << " years old." << std::endl;
}

return 0;
}
1
2
3
4
5
6
7
8
9
10
// 检查键是否存在:
if (myMap.find(key) != myMap.end()) {
// 键存在
}
// 删除元素:
myMap.erase(key);
// 清空 map:
myMap.clear();
// 获取 map 的大小:
size_t size = myMap.size();

vector

定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <vector>

// 创建 Vector
std::vector<int> myVector; // 创建一个存储整数的空 vector
std::vector<int> myVector(5); // 创建一个包含 5 个整数的 vector,每个值都为默认值(0)
std::vector<int> myVector(5, 10); // 创建一个包含 5 个整数的 vector,每个值都为 10
std::vector<int> vec2 = {1, 2, 3, 4}; // 初始化一个包含元素的 vector

// 添加元素
myVector.push_back(7); // 将整数 7 添加到 vector 的末尾

// 访问元素
int x = myVector[0]; // 获取第一个元素
int y = myVector.at(1); // 获取第二个元素

// 获取大小
int size = myVector.size(); // 获取 vector 中的元素数量

// 删除元素
myVector.erase(myVector.begin() + 2); // 删除第三个元素..

// 清空 Vector
myVector.clear(); // 清空 vector

// 嵌套
vector<vector<int>> res(n, vector<int>(n, 0)); // 使用vector定义一个二维数组
res[i][j] = count++;

// 遍历
for (int a : A) {
for (int b : B) {
umap[a + b]++;
}
}

添加元素

如果vector变量的大小没有预先定义,你需要先调用resize函数来设置它们的大小,然后再使用索引来赋值。或者,你可以使用push_backemplace_back来添加元素。

以下是使用resize和索引赋值的示例:

1
2
3
4
5
6
7
8
9
10
nextPts.resize(prevPts.size());
status.resize(prevPts.size());

for (size_t i = 0; i < prevPts.size(); ++i) {
// ...
nextPts[i] = prevPts[i] + flow_at_point;
// ...
status[i] = 0;
// ...
}

以下是使用push_back添加元素的示例:

1
2
3
4
5
6
7
for (size_t i = 0; i < prevPts.size(); ++i) {
// ...
nextPts.push_back(prevPts[i] + flow_at_point);
// ...
status.push_back(0);
// ...
}

如果你知道vector的最终大小,那么使用resize函数来预先设置大小,然后使用索引来赋值,通常会更快。这是因为resize函数会一次性分配足够的内存,而不需要在每次添加元素时重新分配和复制内存。此外,使用索引来赋值可以直接访问和修改vector中的元素,而不需要调用push_backemplace_back函数。

如果你不知道vector的最终大小,或者vector的大小可能会动态变化,那么使用push_backemplace_back来添加元素可能会更方便。这是因为push_backemplace_back函数可以在不知道vector最终大小的情况下动态添加元素。但是,你需要注意的是,如果vector的大小增长很快,push_backemplace_back函数可能需要多次重新分配和复制内存,这可能会影响性能。

总的来说,如果你知道vector的最终大小,推荐使用resize和索引赋值。如果你不知道vector的最终大小,或者vector的大小可能会动态变化,推荐使用push_backemplace_back

clear与容量

clear

在C++中,你可以使用vectorclear方法来清空vector中的所有元素。以下是一个例子:

1
curBoxesLeft_.clear();

在这个例子中,curBoxesLeft_.clear()将清空curBoxesLeft_中的所有元素。这个操作将使curBoxesLeft_的大小变为0,但不会改变它的容量。如果你希望同时清空vector的元素和容量,你可以使用swap方法:

1
vector<BoundingBox>().swap(curBoxesLeft_);

在这个例子中,vector<BoundingBox>().swap(curBoxesLeft_)将创建一个新的空vector,然后与curBoxesLeft_交换。这个操作将使curBoxesLeft_的大小和容量都变为0。

然后,你可以使用push_backemplace_back方法向curBoxesLeft_添加新的目标检测框。例如:

1
2
3
BoundingBox newBox;
// ... 设置newBox的属性...
curBoxesLeft_.push_back(newBox);

在这个例子中,我们首先创建了一个新的BoundingBox对象newBox,然后将其添加到curBoxesLeft_中。

在C++中,vectorclear方法会移除vector中的所有元素,但不会释放内存。如果vector是空的(即,没有赋过值),那么clear操作实际上什么也不会做,但这并不会导致问题。

以下是一个例子:

1
2
std::vector<int> v;
v.clear(); // 这是安全的,即使v是空的

在这个例子中,我们创建了一个空的vector,然后对其执行了clear操作。这不会导致错误或异常。

容量

在C++中,vector的容量(capacity)是指vector在不重新分配内存的情况下可以存储的元素的最大数量。这个值通常大于或等于vector的大小(size),vector的大小是指它当前实际包含的元素的数量。

当你向vector添加元素时,如果vector的大小超过了它的容量,那么vector会重新分配内存,以便能够存储更多的元素。这个过程可能会消耗一些时间,因此,如果你知道vector将需要存储大量的元素,你可以使用reserve方法来预先分配足够的内存,这样可以提高代码的性能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
vector<int> v;

cout << "Size: " << v.size() << endl; // 输出:Size: 0
cout << "Capacity: " << v.capacity() << endl; // 输出:Capacity: 0

v.push_back(1);

cout << "Size: " << v.size() << endl; // 输出:Size: 1
cout << "Capacity: " << v.capacity() << endl; // 输出:Capacity: 1 或更大的数

v.reserve(100);

cout << "Size: " << v.size() << endl; // 输出:Size: 1
cout << "Capacity: " << v.capacity() << endl; // 输出:Capacity: 100

在这个例子中,我们首先创建了一个空的vector,然后添加了一个元素,然后预先分配了足够的内存来存储100个元素。你可以看到,vector的大小和容量在这个过程中是如何变化的。

其它

  1. std::vector的赋值操作符(=)执行的是内容的复制,而不是地址的复制。

string

使用

1
2
3
4
#include <string>
std::string str1 = "Hello, ";
std::string str2 = "World!";
std::string result = str1 + str2;

赎金信

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
for (int i = 0; i < magazine.length(); i++) { // 与.size()相同。
for (int j = 0; j < ransomNote.length(); j++) {
// 在ransomNote中找到和magazine相同的字符
if (magazine[i] == ransomNote[j]) {
ransomNote.erase(ransomNote.begin() + j); // ransomNote删除这个字符
break;
}
}
}
// 如果ransomNote为空,则说明magazine的字符可以组成ransomNote
if (ransomNote.length() == 0) {
return true;
}
return false;
}
};

mutex

互斥量(mutex),它是一种同步原语,用于保护共享数据免受多个线程同时访问。在多线程环境中,如果多个线程试图同时访问和修改同一块数据,可能会导致数据不一致和未定义的行为。为了防止这种情况,我们可以使用互斥量来确保在任何时候只有一个线程能够访问该数据。

比如在回调函数中被持续赋值,在其它函数中被修改。

即使变量在其他函数中只被读取,也需要保护它,因为在多线程环境中,一个线程可能在另一个线程正在写入变量的同时读取该变量,这可能会导致读取到的数据是不一致或者无效的。这种情况被称为“读-写冲突”。

一个或多个共享资源(例如,一个全局变量或一个在多个线程之间共享的数据结构),当一个线程想要访问这个共享资源时,它需要首先锁定(lock)互斥量。如果互斥量已经被另一个线程锁定,那么这个线程将会被阻塞,直到互斥量被解锁(unlock)。当线程完成对共享资源的访问后,它需要解锁互斥量,以便其他线程可以锁定互斥量并访问共享资源。

lock

1
2
3
4
std::mutex m_buf_event;
m_buf_event.lock();
// ... do some work ...
m_buf_event.unlock();

你需要手动调用lock()unlock()来锁定和解锁互斥量。这种方式的问题是,如果在lock()unlock()之间的代码抛出了异常,那么unlock()可能永远不会被调用,从而导致死锁。

示例:

1
2
3
4
5
6
7
8
9
std::mutex m_buf_event;

void eventLeft_callback(const dvs_msgs::EventArray &event_msg){
m_buf_event.lock();
if (!events_left_buf.empty())
events_left_buf.pop();
events_left_buf.push(event_msg);
m_buf_event.unlock();
}

lock_guard

在C++中,std::lock_guard对象的作用域是由其所在的代码块(即最近的大括号{}内的区域)决定的。当std::lock_guard对象在代码块内创建时,它会自动锁定传递给它的互斥量。当std::lock_guard对象超出其作用域(即离开其所在的代码块)时,它的析构函数会被调用,从而自动解锁互斥量。

1
2
3
4
5
std::mutex m_buf_event;
{
std::lock_guard<mutex> lock(m_buf_event);
// ... do some work ...
} // mutex is automatically unlocked here

使用了std::lock_guard,这是一个RAII(Resource Acquisition Is Initialization)机制的互斥包装器,它在构造时提供一个已锁定的互斥,并在析构时解锁互斥。这意味着当std::lock_guard对象超出其作用域并被销毁时,互斥量会自动被解锁,即使在lock_guard的作用域内的代码抛出了异常。这样可以避免死锁,并使代码更安全、更易于理解。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
mutex evt_buf_mutex;
mutex events_storage_mutex;

void callback_events(const dvs_msgs::EventArray::ConstPtr &msg) {
// We first lock the event buffers, to avoid any collision
{
lock_guard<mutex> lock(evt_buf_mutex);

// We then append the received events at the back of the current buffer
evt_buffers[buffer_used].insert(evt_buffers[buffer_used].end(),
begin(msg->events), end(msg->events));
}

// Add new events to events_storage
{
lock_guard<mutex> storage_lock(events_storage_mutex);
for (const dvs_msgs::Event &event : msg->events) {
events_storage.push_back(event);
// If the size of events_storage exceeds accumulation_number, we remove
// the oldest event
if (events_storage.size() > accumulation_number) {
events_storage.pop_front();
}
}
}
}

Eigen

Matrix类与Array类

相对于Matrix类提供的线性代数(矩阵)运算,Array类提供了更为一般的数组功能。Array类为元素级的操作提供了有效途径,比如点加(每个元素加值)或两个数据相应元素的点乘。

1
2
Eigen::Matrix<float, N, N> FFT;
(FFT.array().arg()).matrix(); // 将FFT矩阵转换为数组,然后调用arg()函数计算每个元素的相位(即复数的角度),最后再将结果转换回矩阵。

OpenCV

在 OpenCV 中,cv::Mat::at函数的参数顺序是(row, column),对应于(y, x),而不是通常的(x, y)。这是因为在图像处理中,我们通常将图像视为一个二维数组,其中第一个维度是行(对应于y坐标),第二个维度是列(对应于x坐标)。

OpenCV的数据类型

参考链接

S = 有符号整型 U = 无符号整型 F = 浮点型

CV_8U - 8位无符号整数(0…255)

CV_8S - 8位有符号整数(-128…127)

CV_16U - 16位无符号整数(0…65535)

CV_16S - 16位有符号整数(-32768…32767)

CV_32S - 32位有符号整数(-2147483648…2147483647)

CV_32F - 32位浮点数(-FLT_MAX…FLT_MAX,INF,NAN)

CV_64F - 64位浮点数(-DBL_MAX…DBL_MAX,INF,NAN)

而后面的C1、C2、C3是什么意思呢?这里的1、2、3代表的是通道数,比如RGB就是3通道,颜色表示最大为255,所以可以用CV_8UC3这个数据类型来表示;灰度图就是C1,只有一个通道;而带alph通道的PNG图像就是C4,是4通道图片。

负值处理

cv::Mat矩阵可以存储负值并进行运算,但这取决于你选择的数据类型。OpenCV中的cv::Mat可以存储多种类型的数据,包括有符号整数和浮点数。

例如,如果你选择CV_32F(32位浮点数)或CV_64F(64位浮点数)作为你的数据类型,那么你的cv::Mat矩阵就可以存储负值,并且可以进行各种运算,如加法、减法、乘法等。

以下是一个简单的例子:

1
2
3
cv::Mat mat(3, 3, CV_32F, cv::Scalar(-1)); // Creates a 3x3 matrix filled with -1
mat.at<float>(0, 0) = -5; // Sets the value at the first row and first column to -5
std::cout << mat << std::endl; // Prints the matrix

在这个例子中,我们创建了一个3x3的矩阵,所有元素的初始值都是-1,然后我们将第一行第一列的元素值设置为-5。

当你使用OpenCV的imshow函数来可视化一个cv::Mat矩阵时,负值的处理方式取决于矩阵的数据类型。

如果你的cv::Mat矩阵的数据类型是无符号整数(如CV_8U),那么它不能存储负值,任何负值都会被视为零。

如果你的cv::Mat矩阵的数据类型是有符号整数(如CV_8SCV_16S)或浮点数(如CV_32FCV_64F),那么它可以存储负值。在可视化这样的矩阵时,你需要先将矩阵的值规范化到0-255的范围内。你可以使用OpenCV的normalize函数来实现这一点。

以下是一个例子:

1
2
3
4
cv::Mat mat(3, 3, CV_32F, cv::Scalar(-1)); // Creates a 3x3 matrix filled with -1
cv::normalize(mat, mat, 0, 255, cv::NORM_MINMAX, CV_8U); // Normalize the values to 0-255
cv::imshow("Normalized Image", mat); // Display the normalized image
cv::waitKey(0); // Wait for a key press

在这个例子中,我们首先创建了一个3x3的矩阵,所有元素的初始值都是-1。然后我们使用cv::normalize函数将矩阵的值规范化到0-255的范围内,并将数据类型转换为CV_8U。最后,我们使用cv::imshow函数显示规范化后的图像。

访问cv::Mat对象中特定位置的像素值

在OpenCV中,有几种方法可以访问cv::Mat对象中特定位置的像素值。以下是一些常见的方法:

  1. 使用at函数:这是最直接的方法,你可以使用模板参数来指定像素的数据类型。例如,如果你的图像是一个8位单通道图像,你可以这样访问像素值:

    1
    unsigned char pixel_value = cur_event_mat_left_fft.at<unsigned char>(e_left.y, e_left.x);

    在你的代码中,你使用了cur_event_mat_left_fft.at<char>(i, j)来访问像素值。这里的char是一个有符号的8位整数,范围是-128到127。如果一个像素的真实值是255(在unsigned char中表示为255),在char中它会被表示为-1。

    为了解决这个问题,你应该使用unsigned char来访问cv::Mat的数据。

  2. 使用ptr函数:这个函数返回一个指向图像某一行的指针,然后你可以像操作普通数组一样操作这个指针。这个方法通常比at函数快,但是也更容易出错,因为你需要自己管理指针。例如:

    1
    2
    unsigned char* row_ptr = cur_event_mat_left_fft.ptr<unsigned char>(e_left.y);
    unsigned char pixel_value = row_ptr[e_left.x];
  3. 使用迭代器:你也可以使用C++的迭代器来访问cv::Mat中的像素。这个方法比较安全,但是通常比atptr函数慢。例如:

    1
    2
    cv::Mat_<unsigned char>::iterator it = cur_event_mat_left_fft.begin<unsigned char>() + e_left.y * cur_event_mat_left_fft.cols + e_left.x;
    unsigned char pixel_value = *it;

以上三种方法都可以用来访问和修改像素值。你可以根据你的需要选择最适合你的方法。

赋值

在OpenCV中,cv::Mat的赋值操作符(=)实际上是创建了一个新的头部,但是数据是共享的。这意味着,如果你有两个cv::Mat对象AB,并且你执行了A = B;,那么AB将共享相同的数据。如果你修改了A中的数据,B中的数据也会被修改。

这是因为cv::Mat使用了引用计数机制来管理数据。当你创建一个新的cv::Mat对象并赋值给另一个cv::Mat对象时,它们都会指向同一个数据,而且这个数据的引用计数会增加。当一个cv::Mat对象被销毁时,它会减少数据的引用计数。只有当引用计数变为0时,数据才会被释放。

如果你希望创建一个cv::Mat的真正副本,你可以使用clonecopyTo方法。例如,cv::Mat B = A.clone();将创建一个新的cv::Mat对象B,它包含了A的一个副本。在这种情况下,AB不会共享数据。

clone和copyTo

在OpenCV中,clonecopyTo函数都可以用来复制cv::Mat对象,但它们的使用方式和行为有一些不同。

clone函数创建一个新的cv::Mat对象,并复制源对象的所有数据。它不需要一个已经存在的目标对象,因为它会创建一个新的对象。例如:

1
2
3
cv::Mat src;
// ... 填充src...
cv::Mat dst = src.clone();

copyTo函数将源对象的数据复制到目标对象。如果目标对象已经存在,它的大小和类型必须与源对象匹配,否则它会被重新分配。copyTo还有一个可选的参数,允许你指定一个掩码,只有掩码中非零的元素才会被复制。例如:

1
2
3
cv::Mat src, dst;
// ... 填充src...
src.copyTo(dst);

总的来说,如果你只需要复制一个cv::Mat对象,并且不需要使用掩码,那么clone可能是更简单的选择。如果你需要使用掩码,或者你已经有一个目标对象并希望复制数据到这个对象,那么copyTo可能是更好的选择。

组织管理一组变量或函数

在C++中,除了结构体(struct),还有以下几种方式可以组织和管理一组变量或函数:

  1. 类(Class):类是C++中的一个核心概念,它可以包含变量(称为成员变量)和函数(称为成员函数)。类提供了封装、继承和多态等面向对象编程的特性。
  2. 命名空间(Namespace):命名空间可以用来组织一组相关的变量和函数,以避免命名冲突。
  3. 数组(Array)和向量(Vector):如果你有一组相同类型的变量,你可以使用数组或向量来存储它们。
  4. 函数(Function):如果你有一组相关的操作,你可以将它们封装在一个函数中。
  5. 枚举(Enum):如果你有一组相关的常量,你可以使用枚举来定义它们。
  6. 联合(Union):联合是一种特殊的数据类型,它可以存储不同类型的数据,但一次只能存储其中一种类型的数据。

选择哪种方式取决于你的具体需求和使用场景。

Python

if name == "main":

在Python中,if __name__ == "__main__": 是一个常见的模式。这行代码的作用是检查当前的模块是被直接运行还是被导入为一个模块。

当Python解释器读取一个源文件时,它会首先定义一些特殊的变量。其中一个就是 __name__。如果该文件被直接运行,那么 __name__ 的值会被设置为 "__main__"。如果该文件被其他Python文件导入,那么 __name__ 的值则会被设置为该文件的名字。

因此,if __name__ == "__main__": 这行代码的意思是,"如果这个文件被直接运行,那么执行以下的代码"。这个模式常常被用来在一个Python文件中编写一些测试代码,这些测试代码只有在文件被直接运行时才会执行,而在文件被导入时不会执行。

读取文件

1
2
3
import rosbag
bag_data = rosbag.Bag(rosbag_file, "r")
bag_data.close()

在Python中,使用with语句打开文件时,当with语句的代码块执行完毕后,文件会自动关闭。所以,你不需要显式地调用file.close

这是因为with语句创建了一个上下文,当离开这个上下文时,Python会自动清理相关的资源。在这个例子中,相关的资源就是打开的文件。

1
2
3
4
5
6
7
8
9
import yaml
import numpy as np

# 打开并读取YAML文件
with open('myFolder/cam_to_cam.yaml', 'r') as file:
data = yaml.safe_load(file)

# 从YAML数据中获取矩阵
T_10 = np.array(data['extrinsics']['T_10'])

data = yaml.safe_load(file)这行代码执行完毕后,file会自动关闭,无需手动关闭。

读取参数

1
2
3
4
5
6
7
8
9
10
11
12
13
# python run.py file.rosbag /ground_truth/odometry /pose_graph/odometry

import sys

if __name__ == "__main__":
# 从命令行参数获取rosbag的路径和指定的topics
rosbag_file = sys.argv[1]
gt_topic_name = (
sys.argv[2] if len(sys.argv) > 2 else "/ground_truth/odometry"
)
est_topic_name = (
sys.argv[3] if len(sys.argv) > 3 else "/pose_graph/odometry"
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# python run.py file.rosbag /ground_truth/odometry /pose_graph/odometry --start_offset 0.5 --end_offset 0.5

import argparse
import os

if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Filter ROS bag messages based on timestamp ranges.")
parser.add_argument("input_bag_path", help="Path to the input ROS bag file.")
parser.add_argument("gt_topic_name", help="Ground truth topic name.", default="/ground_truth/odometry")
parser.add_argument("est_topic_name", help="Estimated topic name.", default="/pose_graph/odometry")
parser.add_argument("--start_offset", type=float, default=0.0, help="Offset to add to the start time (in seconds).")
parser.add_argument("--end_offset", type=float, default=0.0, help="Offset to subtract from the end time (in seconds).")
args = parser.parse_args()

output_bag_path = os.path.splitext(args.input_bag_path)[0] + "_alignment.bag"

filter_bag(args.input_bag_path, args.gt_topic_name, args.est_topic_name, output_bag_path, args.start_offset, args.end_offset)

初始化列表

用法

在C++中,初始化列表用于在构造函数中初始化类的成员变量。它允许在进入构造函数体之前对成员变量进行初始化,从而提高效率和简洁性。以下是初始化列表的语法和用法:

1
2
3
4
5
6
7
8
class MyClass {
public:
int x;
int y;
MyClass(int a, int b) : x(a), y(b) {
// 构造函数体,可以包含其他初始化逻辑
}
};

在这个例子中,MyClass有两个成员变量xy,它们在构造函数的初始化列表中被初始化为ab的值。

初始化列表的优点包括:

  • 提高性能:避免了成员变量的默认初始化和随后赋值的开销。
  • 初始化常量成员:可以初始化const成员和引用成员。
  • 初始化基类:可以在派生类的初始化列表中调用基类的构造函数。

其它用法

除了类,C++中的初始化列表还可以用于以下场景:

  • 结构体

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    struct Point {
    int x;
    int y;
    Point(int a, int b) : x(a), y(b) {}
    };

    int main() {
    Point p(10, 20);
    std::cout << "Point: (" << p.x << ", " << p.y << ")" << std::endl;
    return 0;
    }
  • 数组

    1
    int arr[3] = {1, 2, 3};
  • 标准库容器(如std::vector, std::list, std::map等):

    1
    2
    std::vector<int> vec = {1, 2, 3, 4, 5};
    std::map<int, std::string> m = {{1, "one"}, {2, "two"}, {3, "three"}};
  • 初始化列表构造函数,使用std::initializer_list

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class MyClass {
    public:
    MyClass(std::initializer_list<int> initList) {
    for (int val : initList) {
    // Do something with val
    }
    }
    };

    MyClass obj = {1, 2, 3, 4, 5};
  • 范围for循环

    1
    2
    3
    for (int i : {1, 2, 3, 4, 5}) {
    // Do something with i
    }

这些示例展示了初始化列表在C++中的广泛应用,不仅限于类和结构体。

内存泄漏

参考链接

什么是内存泄漏?

内存泄漏是指程序中已分配的内存未能(在离开其作用域、程序运行结束后)成功释放,导致可用内存逐渐减少的现象。在程序运行过程中,如果反复发生内存泄漏,最终可能会导致系统可用内存耗尽,从而影响程序的性能或导致程序崩溃。内存泄漏在长时间运行的程序中尤其危险,例如服务器或持续运行的后台任务。

内存泄漏的原因

内存泄漏通常发生在以下几种情况:

  • 未释放动态分配的内存:当使用如 malloc, calloc, reallocnew 等函数分配内存后,未使用对应的 free 或 delete 来释放内存。

  • 资源占用:除了内存外,程序可能申请其他系统资源(如文件句柄、数据库连接等),未正确释放这些资源也会导致类似内存泄漏的问题。

  • 数据结构错误:例如,链表、树等数据结构若未正确处理其元素的删除操作,可能导致部分节点成为不可达的,从而造成内存泄漏。

如何判断内存泄漏?

判断和诊断内存泄漏通常需要以下几个步骤或工具:

  1. 代码审查:通过审查代码来寻找可能未释放内存的地方。特别关注那些有动态内存分配的函数或模块。
  2. 运行时工具:
    • Valgrind:这是一个编程工具,用于内存调试、内存泄漏检测等。在 Linux 环境下,使用 Valgrind 运行程序可以帮助检测内存泄漏。
    • Visual Studio:在 Windows 环境下,Visual Studio IDE 提供了内置的内存泄漏检测工具。
    • Sanitizers:如 AddressSanitizer,这是一种快速的内存错误检测工具,可以集成到 GCC 或 Clang 编译器中,用于检测内存泄漏和其他内存相关错误。
  3. 性能监控工具:使用系统或第三方性能监控工具来观察程序的内存使用情况,查看内存使用是否随时间持续增加。
  4. 日志和追踪:在代码中添加日志输出,特别是在分配和释放资源的地方,可以帮助追踪内存的使用和释放。

如何防止内存泄漏?

  • 使用智能指针:在 C++中使用 std::unique_ptr, std::shared_ptr 等智能指针可以自动管理内存,大大减少内存泄漏的风险。
  • 资源获取即初始化(RAII, Resource Acquisition Is Initialization):这是一种编程范式。资源的获取即是初始化,资源的释放即是销毁。确保在对象的生命周期内资源被正确管理。通过在对象的构造函数中分配资源,并在析构函数中释放资源,可以保证资源总是被正确管理。
  • 定期代码审查:定期进行代码审查可以帮助识别潜在的内存泄漏问题。
  • 自动化测试:编写测试用例,尤其是针对资源管理的单元测试,可以在开发过程中早期发现和解决内存泄漏问题。

溢出

1
2
long goal = nums[i] + nums[j];
if (goal > target) right--;

在这段代码中,nums[i]nums[j] 是两个整数(假设它们是 int 类型)。nums[i] + nums[j] 表达式会先执行整数相加操作,然后将结果转换为 long 类型并赋值给 goal。如果 nums[i] + nums[j] 的结果超过了 int 类型的范围(即发生溢出),则在转换为 long 类型之前已经丢失了信息,因此可能会导致错误的结果。

1
if ((long) nums[i] + nums[j] > target) right--;

在这段代码中,(long) nums[i] + nums[j] 会先将 nums[i] 转换为 long 类型,然后再进行加法运算。由于 long 类型的范围比 int 类型大得多,这样可以避免在加法运算过程中发生溢出。因此,即使 nums[i]nums[j] 的和超过了 int 类型的范围,它们的和仍然可以正确地表示为 long 类型,不会丢失信息。

如果你仍然想使用 goal 变量来代表 nums[i] + nums[j],并且避免数值溢出的问题,可以在计算和赋值时进行显式类型转换。你可以先将 nums[i] 转换为 long 类型,再进行加法运算,并将结果赋值给 goal 变量。以下是正确的做法:

1
2
long goal = static_cast<long>(nums[i]) + nums[j];
if (goal > target) right--;

当一个 long 类型的变量与一个 int 类型的变量相加时,int 类型的变量会被自动提升为 long 类型,然后进行相加运算。这样可以确保运算结果的正确性和避免潜在的溢出问题。

MATLAB转C++

https://www.zhihu.com/

MATLAB做完之后,只要自己封装的函数参数足够简介,用自动生成的C++代码足够应用在工程了,与手写没什么区别。

是滴,我现在很多函数都是MATLAB写好然后转化成C代码直接调用,但是这种调用每次程序启动时会比较慢,你有什么解决的方法吗?

我感觉还好,可以试试指定变量类型,能用整数就不用double转换,能定点就改定点,如果是编译成库,尽量静态,是不是会好一点?

得分情况,如果程序里有涉及大矩阵运算,或者是信号处理的程序,MATLAB肯定比咱们自己写的C快,但如果涉及大量复杂循环过程,且很多过程没法矩阵化,那MATLAB速度确实比不上C。不过对于快速验证来说,MATLAB要比C方便很多。

请问MATLAB转C或C++有啥快速方法吗?还是需要一句一句转?

有一条指令,在MATLAB的command里面输入

1
2
mcc -w cpplib:funcname -T
link:lib funcname.m -C

具体可以网上搜一下。

静态代码分析

https://www.zhihu.com/

想问一下,有没有能够获取C++中各个方法之间的调用依赖关系的开源软件?

可以尝试Understand。

可以调试的话,直接GDB断点,明明白白。

Source Insight?没Understand好用。

其它

  1. code单词做代码释义时是不可数名词。

    1
    2
    不可数名词
    Computer code is a system or language for expressing information and instructions in a form which can be understood by a computer.
  2. Python 并没有强制要求你用 Tab 缩进或者用空格缩进,但在PEP8中,建议使用4个空格来缩进。对于任何一个编辑器或者IDE,一般都有配置选项,可以设置把 TAB 键展开为4个空格,以保证代码的兼容性。

  3. 命令行使用\实现换行:

    1
    2
    3
    sudo apt-get install \
    ros-$1-sophus \
    ros-$1-pcl-ros
  4. 报错:SyntaxError: Non-ASCII character '\xe5'

    原因:Python默认是以ASCII作为编码方式的,如果在自己的Python源码中包含了中文(或者其他非英语系的语言),此时即使你把自己编写的Python源文件以UTF-8格式保存了,但实际上,这依然是不行的。

    解决:在源代码的第一行加入:

    1
    # -*- coding: UTF-8 -*-
  5. python引入本地字体:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import matplotlib.font_manager as fm
    fm.fontManager.addfont('/usr/share/fonts/truetype/Times-New-Roman/times.ttf')
    fm.fontManager.addfont('/usr/share/fonts/truetype/simsun/simsun.ttc')

    plt.title('层次聚类树形图', fontname="simsun", fontsize=30)
    plt.xticks(fontname="Times New Roman", fontsize=26)

    colorbar = plt.colorbar()
    colorbar.ax.tick_params(labelsize=24) # 设置坐标轴标签的字体大小
    for l in colorbar.ax.yaxis.get_ticklabels():
    l.set_family('simsun')
  6. idea错误提示File was loaded in the wrong encoding: ‘UTF-8‘解决方法:

    1. 打开乱码文件,在软件右下角将当前页面的编码格式改为GB2312,弹出的提示消息中选择Reload;
    2. 在软件右下角将当前页面的编码格式改为utf-8,弹出的提示消息中选择Convert;
    3. 参考链接
  7. 如果你不想运行predict.py文件的main函数第136行以下的代码,你可以使用Python的return语句来提前结束函数的执行。你需要找到第136行的代码,并在其前面添加return语句。这将导致函数在执行到这一行时立即返回,不再执行后续的代码。

    在循环语句里,可以使用continue

  8. syntax error near unexpected token '$'{\r''

    字面意思上看是换行符出现问题,怀疑是Win下编辑过。

    1
    2
    3
    # 用vim -b 查看,发现每一行多了~M
    # 解决方法:
    sed -i 's/\r//g' xxx.sh
  9. 变量的定义和使用只在最近的{}内((主)函数、iffor等)适用。如果想要拓展变量的使用范围,可以在更外处的{}内定义变量(然后在别处赋值);或者,声明为全局变量。

  10. 在C++中,函数的默认参数值通常在函数声明中给出,也就是在.h头文件中(而不用在函数定义的*.cpp文件中再给出默认值)。这样,任何包含这个头文件的代码都可以看到这些默认值,并且可以选择是否提供自己的参数值。

  11. 一般来说,如果有 3 个或更多if-else分支,则应该考虑使用switch。如果有10个或更多条件分支,您应该考虑使用config变量或文件,并为该config编写特定的函数来进行映射。如果映射逻辑复杂但使用频繁,可以考虑创建专用的规则引擎或DSL来处理。

    if语句和switch语句的选择:多个等值判断选择switch,其它情况(区间范围等)选择if

  12. 在C++中,i++++i都会增加i的值,但它们的主要区别在于它们的返回值和执行顺序。

    i++是后置递增运算符。它首先返回i的当前值,然后再将i的值增加1。例如:

    1
    2
    int i = 5;
    int j = i++; // j现在是5,i现在是6

    ++i是前置递增运算符。它首先将i的值增加1,然后返回新的i值。例如:

    1
    2
    int i = 5;
    int j = ++i; // j现在是6,i现在也是6
  13. 在C++中,#include <filename>#include "filename"的主要区别在于编译器搜索头文件的方式。

    • #include <filename>:编译器在标准库路径中搜索头文件。这通常用于包含标准库的头文件,如<iostream><vector><mutex>等。
    • #include "filename":编译器首先在当前文件的目录中搜索头文件,如果在当前目录中找不到,编译器会在标准库路径中搜索。这通常用于包含用户自定义的头文件。
  14. variable的生命周期取决于它在哪里声明和定义。如果它在while循环外部定义,那么它将在包含该循环的函数或作用域结束时被销毁。如果它在while循环内部定义,那么它将在每次循环结束时被销毁,并在下一次循环开始时重新创建。

  15. 把函数的输入参数行想成是对输入参数的定义,把函数的调用行想成是对输入参数的赋值,函数内部就是变量赋值后的进一步操作。

  16. 变量l(包括函数的输入参数)被定义为const,这意味着它的值在初始化后不能被修改。这是一种良好的编程实践,可以防止在后续的代码中意外修改l的值。

    然而,如果你确定在后续的代码中不会修改l的值,那么将l定义为const并不是必须的。在这种情况下,将l定义为const主要是为了提高代码的可读性和可维护性,它可以让其他阅读你代码的人知道l的值在初始化后不应该被修改。

    总的来说,是否将l定义为const取决于你的编程风格和项目的编码规范。如果你的项目鼓励使用const来增强代码的可读性和可维护性,那么将l定义为const是一个好的选择。

  17. 命令行参数:

    1
    2
    3
    4
    import sys

    if __name__ == "__main__":
    npy_file = sys.argv[1] if len(sys.argv) > 1 else __file__

    sys.argv 是一个包含命令行参数的列表,其中 sys.argv[0] 是脚本的名称,sys.argv[1] 是第一个参数,sys.argv[2] 是第二个参数,以此类推。

  18. 初始化 ROS 工作空间:

    1
    alias initROS="mkdir -p catkin_ws/src && cd catkin_ws && catkin config --init --mkdirs --extend /opt/ros/melodic --merge-devel --cmake-args -DCMAKE_BUILD_TYPE=Release && cd src && catkin_init_workspace && cd .. && catkin build"
  19. i++是先赋值,然后再自增;++i是先自增,后赋值。

  20. C++ 中 for 循环的语法:

    1
    2
    3
    4
    5
    for ( init; condition; increment )
    {
    statement(s);
    }

    下面是 for 循环的控制流:

    1. init 会首先被执行,且只会执行一次。这一步允许您声明并初始化任何循环控制变量。您也可以不在这里写任何语句,只要有一个分号出现即可。
    2. 接下来,会判断 condition。如果为真,则执行循环主体。如果为假,则不执行循环主体,且控制流会跳转到紧接着 for 循环的下一条语句。
    3. 在执行完 for 循环主体后,控制流会跳回上面的 increment 语句。该语句允许您更新循环控制变量。该语句可以留空,只要在条件后有一个分号出现即可。
    4. 条件再次被判断。如果为真,则执行循环,这个过程会不断重复(循环主体,然后增加步值,再然后重新判断条件)。在条件变为假时,for 循环终止。
  21. 针对多组输入输出且数据没有固定数据量我们通常这样解决问题:采用while(scanf("%d",&n) != EOF)while(~scanf(“%d”, &n))。EOF全称是End Of File(C语言标准函数库中表示文件结束符)。

  22. 在C++编程中,推荐在每个for循环中直接定义和初始化变量i,即for(int i = 0; i < n; i++)。这样做的好处包括:

    • 作用域管理:每个i变量的作用域仅限于对应的for循环内部,避免了不同循环之间的命名冲突。
    • 代码可读性:更容易理解和维护代码,因为变量i的作用范围明确。
  23. 变量定义位置两种写法各有优缺点:

    • 写法一tmptmp1变量在每次循环开始时定义,作用域仅限于循环体内。这样可以减少变量的生命周期,降低潜在的错误风险。
    • 写法二tmptmp1变量在循环外定义,作用域覆盖整个循环。这样可以减少变量定义的次数,可能在性能上略有提升。

    一般来说,推荐使用写法一,因为它使变量的作用域更清晰,减少了意外修改变量的风险。

  24. 在C++中,NULLnullptr用于表示空指针,但它们有一些区别:

    • NULL:传统的C风格的空指针常量,通常定义为整数类型的0。
    • nullptr:C++11引入的新关键字,专门用于表示空指针,类型是std::nullptr_t

    nullptr更安全和明确,因为它专门用于指针,而NULL是一个整数常量,可能导致类型不匹配的问题。

  25. 在C++中,int* pint *p两种写法都是有效的,表示相同的含义,即p是一个指向int类型的指针。选择哪种写法主要是风格问题:

    • int* p;:强调p是一个指针类型。
    • int *p;:强调*p是一个int类型的变量。

    两种写法都被广泛使用,选择哪种主要取决于个人或团队的编码规范。

  26. 在C++中,new关键字用于在堆上动态分配内存,并调用相应的构造函数来初始化对象。以下是new关键字的用法:

    new关键字通常用于动态分配内存并返回指向该内存的指针变量。这样可以在运行时灵活地管理内存,用于创建对象或数组。

    1. 分配单个对象

      1
      2
      int* p = new int; // 分配一个int类型的对象
      *p = 5;
    2. 分配并初始化单个对象

      1
      int* p = new int(5); // 分配并初始化一个int类型的对象
    3. 分配数组

      1
      int* arr = new int[10]; // 分配一个包含10个int类型元素的数组
    4. 分配自定义类型的对象

      1
      2
      3
      4
      5
      struct MyStruct {
      int x;
      MyStruct(int val) : x(val) {}
      };
      MyStruct* obj = new MyStruct(10); // 分配并初始化MyStruct类型的对象

    记得在使用完动态分配的内存后使用deletedelete[]释放它,以避免内存泄漏。

  27. 在C++中,关于变量声明的编程规范可能因团队或项目的编码风格指南而异。一般来说:

    • int i, j; 一行中声明多个变量,节省空间,适合声明类型相同且相关的变量。
    • int i; int j; 每行声明一个变量,清晰明了,易于维护和调试。

    很多编码规范推荐每行声明一个变量,以提高代码的可读性和可维护性。

  28. begin()end()是C++标准库容器类(如vector, array, list, unordered_set等)提供的成员函数,用于获取容器的迭代器。

    1. begin()返回一个指向容器中第一个元素的迭代器。如果容器为空,begin()返回的迭代器等于end()

    2. end()返回一个指向容器末尾后一个位置的迭代器(即一个“哨兵”位置,不指向任何有效元素)。end()迭代器通常用于在循环中判断是否已经到达容器的末尾。

  29. 路径中有空格:

    1
    2
    3
    ~/Documents/test file/file_1
    ~/Documents/'test file'/file_1
    ~/Documents/test\ file/file_1
  30. cd ${rosbag_dir}还是cd "${rosbag_dir}"更好些?

    在处理路径时,使用引号可以避免由于路径中包含空格或特殊字符而导致的问题。因此,cd "${rosbag_dir}" 更好些。

  31. for循环,多个循环变量:

    1
    2
    3
    4
    5
    void reverseString(vector<char>& s) {
    for (int i = 0, j = s.size() - 1; i < s.size()/2; i++, j--) {
    swap(s[i],s[j]);
    }
    }
  32. 等等。


编程知识点
http://zeyulong.com/posts/f576656f/
作者
龙泽雨
发布于
2025年1月8日
许可协议