编程知识点
本文最后更新于 2025年1月8日 上午
本文主要分享了编程过程中积累的知识点。
编译与卸载
编写CmakeLists.txt
在CmakeLists.txt
里指定第三方库所在的路径
在CmakeLists.txt
里指定第三方库所在的路径,即指定其编译安装后.cmake
文件所在的路径,例如:
1 |
|
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_DIRS
和OpenCV_LIBS
,这些变量分别包含了OpenCV的头文件路径和库文件路径,可以在后续的target_include_directories
和target_link_libraries
命令中使用。
如果没有设置
OpenCV_DIR
,find_package
命令会在默认的路径下查找OpenCV库。这些默认的路径包括:
- CMake的模块路径(
CMAKE_MODULE_PATH
)- CMake的安装前缀(
CMAKE_PREFIX_PATH
)- 系统的环境变量路径
具体来说,
find_package
会查找名为OpenCVConfig.cmake
或opencv-config.cmake
的文件,这个文件通常位于OpenCV库的安装目录中。如果你的OpenCV库安装在非标准的位置,或者你有多个版本的OpenCV库并且想要选择一个特定的版本,那么你可以通过设置
OpenCV_DIR
来指定OpenCV库的路径。如果没有设置OpenCV_DIR
,CMake可能会找到错误的版本或者找不到OpenCV库。
OpenCV_DIR
和OpenCV_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
里也可以看到指定的版本。
其实就类似于在系统中
cmake
、make
和make install
的步骤,只不过这里的第三方库是安装在了ROS工作区里被相互调用,catkin clean
后也就删除掉了,而没有安装在系统环境里。也方便使用指定版本的第三方库。
1 |
|
或者,将src
(这个文件是原本解压下载的第三方源码source的地方,具体名称要看CMakeLists.txt
中SOURCE_DIR的设置)中的各个第三方源码都解压好,放到src对应的文件夹中。例如catkin_ws/build/xxx/xxx_src-prefix/src/xxx.tar.gz
。
cmake
一般流程
1 |
|
我个人推荐把第三方库安装在
/usr/local
文件夹下进行管理。例如,在/usr/local
文件夹下新建文件夹eigen3
,后在eigen3
文件夹下新建文件夹eigen330
、eigen340
。
定义编译参数
可在cmake
命令后加参数:
1 |
|
也可以在CMakeLists.txt
文件中定义,例如,启用参数EFFT_USE_FFTW3
:
1 |
|
或,在CMakeLists.txt
文件中:
1 |
|
保存cmake输出
ROS:
cmake build
的输出在catkin_ws/logs/your_package_name/build.cmake.log、build.make.log
里。
cmake
命令的输出信息通常在终端中显示,而不是保存在文件中。这些信息包括配置过程中的警告、错误以及其他重要信息。
然而,你可以将cmake
命令的输出重定向到一个文件中。例如,你可以使用以下命令将输出保存到一个名为output.txt
的文件中:
1 |
|
在这个命令中,>
操作符将cmake
命令的输出重定向到output.txt
文件中。如果output.txt
文件已经存在,这个命令将覆盖它的内容。如果你想要追加输出到文件中,而不是覆盖它,你可以使用>>
操作符,如下所示:
1 |
|
请注意,这些命令只会捕获标准输出,而不会捕获错误输出。如果你也想要捕获错误输出,你可以使用2>&1
,如下所示:
1 |
|
在这个命令中,2>&1
将错误输出重定向到标准输出,然后>
操作符将标准输出重定向到output.txt
文件中。这样,output.txt
文件将包含所有的输出,包括错误信息。
make
1 |
|
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 |
|
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 rm
和sudo 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 clean
和make distclean
是两个常见的 makefile 目标,它们的功能取决于 makefile 的编写者如何定义它们。但是,通常它们的功能如下:
make clean
:这个命令通常用于删除所有由 makefile 生成的文件。这通常包括编译产生的对象文件(.o 或 .obj 文件)和编译器生成的中间文件。但是,它通常不会删除配置文件或者 makefile 文件。make distclean
:这个命令通常用于将目录恢复到初始状态。除了删除make clean
会删除的文件,它还会删除配置文件和 makefile 文件。这个命令通常在你想要重新配置和编译一个项目时使用。需要注意的是,这两个命令的具体行为取决于 makefile 的编写者。在使用这些命令之前,你应该查看 makefile 或者相关的文档,以了解这些命令的具体行为。
编程命名规范
-
匈牙利命名法(将变量类型写进变量名的命名方法)。
其基本原则是,变量名=属性+类型+对象描述。通过在变量名前面加上相应的小写字母的符号标识作为前缀,标识出变量的作用域,类型等。
这些符号可以多个同时使用,顺序是先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) -
驼峰式命名法,又叫小驼峰式命名法。常用于变量名,函数名。
要求第一个单词首字母小写,后面其他单词首字母大写。
1
2
3int myAge;
char myName[10];
float manHeight; -
帕斯卡命名法,又叫大驼峰式命名法。常用于类名,属性,命名空间等。
与小驼峰式命名法的最大区别在于,每个单词的第一个字母都要大写。
1
2
3int MyAge;
char MyName[10];
float ManHeight; -
下划线命名法。
下划线命名法并不如大小驼峰式命名法那么备受推崇,但是也是浓墨重彩的一笔。尤其在宏定义和常量中使用比较多,通过下划线来分割全部都是大写的单词。还有变量名太长的变量。
该命名规范,也是很简单,要求单词与单词之间通过下划线连接即可。
1
2
3int my_age;
char my_name[10];
float man_height; -
在C++编程中,变量名后加
_
的命名方式通常用于表示类的私有数据成员。这是一种命名约定,用于区分类的数据成员和局部变量,以提高代码的可读性。例如,fftwInput_
、fftwOutput_
和plan_
都是类的私有数据成员,它们的名字都以_
结尾。一般来说,类的数据成员应该被定义为私有(private),这是面向对象编程中的封装原则。通过将数据成员设为私有,可以防止外部代码直接访问或修改这些数据,从而保护类的内部状态的完整性。
然而,有时候,你可能会选择将某些数据成员设为公有(public)。这通常在数据成员是类的公开接口的一部分,或者类本身就是一个简单的数据结构时发生。
至于是否在公有数据成员的名称后加
_
,这完全取决于你的命名约定。在某些命名约定中,可能会在所有数据成员的名称后加_
,无论它们是公有的还是私有的。在其他命名约定中,可能只在私有数据成员的名称后加_
。总的来说,关键是选择一种命名约定,并在整个代码库中一致地遵循它,以提高代码的可读性和一致性。
变量名称前加下划线:这通常用于表示私有成员变量或者类的内部变量。然而,根据C++标准,名称以一个下划线开头的变量可能被保留给编译器的实现,所以一般不推荐这种做法。
-
在编程中,
i
、j
和k
通常用作循环变量,特别是在嵌套循环中。idx
和jdx
是对这些传统变量名的扩展,其中idx
可能表示"index",jdx
可能表示第二个索引。然而,更具描述性的变量名可能会使代码更易于理解。例如,如果
idx
遍历的是当前帧的目标检测框,那么你可能会选择名为curBoxIdx
的变量名。同样,如果jdx
遍历的是前一帧的目标检测框,那么你可能会选择名为prevBoxIdx
的变量名。 -
在我们的系统中,所有环境变量都使用大写字母命名。所以当我们声明局部变量时,应该使用小写字母来声明,以避免环境和局部变量名发生冲突。
-
在命名变量时,使用后缀
dir
和path
有助于区分变量的用途:dir
:通常用于表示一个目录(文件夹)。例如,dataset_dir
表示数据集所在的目录。path
:通常用于表示一个具体的文件路径或目录路径。它可以是一个文件的完整路径,也可以是一个目录的路径。例如,dataset_path
表示数据集的完整路径。
-
使用动词来命名函数是一种常见的做法。例如,我们不命名我们的函数:
dateFormatting
,我们将其命名为:formatDate
。 -
等等。
头文件
C++编程中常用的头文件
C++编程中常用的头文件包括:
- 输入输出流:
<iostream>
:标准输入输出流<fstream>
:文件输入输出流
- 标准库容器:
<vector>
:动态数组<array>
:固定大小数组<list>
:双向链表<deque>
:双端队列<map>
:关联容器(键值对)<set>
:集合<unordered_map>
:无序关联容器<unordered_set>
:无序集合
- 字符串处理:
<string>
:字符串类
- 算法:
<algorithm>
:常用算法(如排序、查找)<numeric>
:数值操作(如累加、乘积)
- 多线程:
<thread>
:线程相关功能<mutex>
:互斥锁<condition_variable>
:条件变量
- 智能指针:
<memory>
:智能指针(如std::unique_ptr
、std::shared_ptr
)
- 其他工具:
<utility>
:实用工具(如std::pair
、std::move
)<functional>
:函数对象和绑定器<chrono>
:时间库<limits>
:数值极限<cmath>
:数学函数<cstdlib>
:通用工具(如动态内存管理、随机数生成)
这些头文件涵盖了C++编程的许多常见需求和功能。
其它头文件
与整数类型限制相关的常量
除了INT_MAX与INT32_MAX,常用的还有哪些?
常用的与整数类型限制相关的常量包括:
<climits>
头文件:INT_MIN
:int
类型的最小值UINT_MAX
:unsigned int
类型的最大值LONG_MIN
:long
类型的最小值LONG_MAX
:long
类型的最大值ULONG_MAX
:unsigned long
类型的最大值LLONG_MIN
:long long
类型的最小值LLONG_MAX
:long long
类型的最大值ULLONG_MAX
:unsigned long long
类型的最大值
<cstdint>
头文件:INT8_MIN
,INT8_MAX
:int8_t
类型的最小值和最大值UINT8_MAX
:uint8_t
类型的最大值INT16_MIN
,INT16_MAX
:int16_t
类型的最小值和最大值UINT16_MAX
:uint16_t
类型的最大值INT32_MIN
,INT32_MAX
:int32_t
类型的最小值和最大值UINT32_MAX
:uint32_t
类型的最大值INT64_MIN
,INT64_MAX
:int64_t
类型的最小值和最大值UINT64_MAX
:uint64_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
-
使用有意义的名字。像
transactionHistory
这样的变量或者像这样的方法validateSufficientBalanceForRefund
清楚地传达他们的目的。 -
遵循单一职责原则(SRP)。通过将不同的职责委托给不同的类,每个类都有一个更改的理由,从而简化了维护并提高了可读性和可测试性。
-
缩短方法。函数应该只具有比其名称低一级的代码。将长方法分解为更小、更集中的方法可以使代码更不容易出错并且更易于维护。它还简化了调试并提高了清晰度。样式指南通常建议将方法保持在 20-30 行左右。如果某个方法超出了此范围,则通常表明该算法过于复杂或该方法试图执行的操作过多。
- 函数应该是可重用的。而且函数越大,可重用的可能性就越小。这也与为什么一个函数应该只做一件事相关。如果它只做一件事,那么它很可能会很小。即,将一个长函数分解为多个分管各个小功能的短函数组成。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25public 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
30public 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;
} -
以有意义的方式使用注释。过度使用注释或添加只是重申代码正在执行的操作的注释是多余的,并且不会增加任何价值。此外,过时或不正确的注释可能会误导开发人员并造成混乱。另外,如果逻辑已修改,请不要忘记更新注释。
-
一致的格式。一致的格式极大地提高了可读性和团队合作。有许多工具可以帮助制定缩进和间距的编码标准。另外,在任何 IDE 中,这都只是一个快捷方式。
-
提供有意义的报错消息。通过捕获特定的异常并提供有意义的错误消息,调试和理解错误变得更加容易。此外,使用记录器记录异常,而不是打印堆栈跟踪,而是将错误集成到集中式日志记录系统中,使它们更易于管理和监控。
-
让你的代码保持在界限内。将代码保持在边缘线内可以轻松快速扫描。 IDE 通常会提供指导原则,通常为每行 80 或 100 个字符(数量可自定义),以帮助遵循此实践。例如,IntelliJ IDEA 甚至提供了边距的可视化表示。此外,将长行分成更小的部分还可以促进更好的编码实践,例如将逻辑封装到命名良好的方法和类中。这简化了代码审查和协作,因为团队成员可以快速掌握代码的结构和意图,而无需排长队。
-
编写有意义的测试用例。有效的测试清晰、简洁,并专注于验证代码的特定行为,包括正常条件、边界情况和潜在错误。它们应该易于其他开发人员理解,明确正在测试的内容和原因。
-
审查你的代码。定期的代码审查对于确保质量、一致性和可维护性至关重要。代码审查对于知识共享和预先识别潜在问题的重要性怎么强调都不为过。永远不要懒惰这样做。更重要的是,始终对那些花时间审查和评论您的代码的人做出回应。确认他们的反馈,以表明他们的声音被听到并且他们的意见受到赞赏。这可以培养团队文化并加强关系。
-
不断改进你的方法。了解何时优先考虑清晰性而非简洁性、简单性而非复杂性以及特殊性而非通用性对于编写有效的代码和成为专业的团队成员至关重要。确保您的代码像您希望其他人的代码一样易于理解。
-
开闭原则 (OCP) 规定类、方法或函数必须对扩展开放,但不能对修改开放。这意味着定义的任何类、方法或函数都可以轻松地重用或扩展用于多个实例,而无需更改其代码。举个例子,我们有一个名为地址的类。
1
2
3
4
5
6
7
8
9
10
11
12
13
14class 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
15capitals = {
'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()) -
避免使用幻数(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" -
避免深层嵌套。限制循环、条件或函数内的嵌套级别以提高可读性。
-
函数的理想参数数量为零。然后我们有一个和两个参数函数。应避免构造具有三个参数的函数。超过三个参数需要特殊的理由——即使这样,也不应该使用这样的函数。如果您有超过 3 个参数,将它们分组到一个对象中可能是一个解决方案。
代码注释
一般注释
C++
1 |
|
python
1 |
|
函数注释
1 |
|
特殊注释
这些是注释中的标签(tag),有时也被称作“代码标签(codetag)”或“标记(token)”。
标识:
TODO
:标记代码中需要实现的功能或任务。FIXME
:标记代码中需要修复的问题或缺陷。XXX
:如果代码中有该标识,说明标识处代码虽然实现了功能,但是实现的方法有待商榷,代码有问题或具误导性,需引起警惕。希望将来能改进,要改进的地方会在说明中简略说明。HACK
/BODGE
/KLUDGE
:标记临时性修复或不优雅的解决方案。英语翻译为砍。如果代码中有该标识,说明标识处代码我们需要根据自己的需求去调整程序代码。BUG
/DEBUG
:标记已知的Bug或错误。UNDONE
:对之前代码改动的撤销。NOTE
:提供额外的注释或提示信息,帮助理解代码意图或设计决策。
格式:
1 |
|
C++数据类型
C++形参和实参
形参和实参是函数中的两个重要概念。
形参(形式参数)是在函数定义中出现的参数,它是一个虚拟参数,只有在函数调用时才会接收到传递进来的实际参数。形参可以被看作是一个占位符,在函数定义时并没有实际的数值,只有在函数调用时才会得到实参的数值。形参的主要作用是表示函数需要从外部传递进来的数据。
实参(实际参数)是在函数中实际出现的参数,它的值可以是常量、变量、表达式、类等。实参的值是确定的,必须在函数调用时提供。实参的主要作用是向函数传递数据,将数据的值传递给形参,在函数体内被使用。
要注意的是,形参和实参之间的传递方式有两种:值传递和地址传递。值传递是指将实参的值复制给形参,形参在函数内部使用时不会改变实参的值。而地址传递是指将实参的地址传递给形参,形参在函数内部使用时可以通过地址修改实参的值。
总结起来,形参是函数定义中的参数,是一个虚拟的占位符,用于接收函数调用时传递进来的实参。实参是函数调用时提供的具体数值,用于向函数传递数据。形参和实参之间的传递方式可以是值传递或地址传递。
1 |
|
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 可应用于整型,signed 和 unsigned 可应用于字符型,long 可应用于双精度型。
这些修饰符也可以组合使用,修饰符 signed 和 unsigned 也可以作为 long 或 short 修饰符的前缀。例如:unsigned long int。
C++ 允许使用速记符号来声明无符号短整数或无符号长整数。您可以不写 int,只写单词 unsigned、short 或 long,int 是隐含的。
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++中,普通函数、类的普通成员函数和类的静态成员函数之间有以下几点主要区别:
- 普通函数:属于全局函数,不受具体类和对象的限制,可以直接调用。
- 普通函数不属于任何类,它只能访问全局变量和其参数。它不能访问类的成员变量和成员函数(除非有一个类的对象或指针作为参数传入)。
- 类的普通成员函数:类的普通成员函数属于类的实例(对象),它可以访问类的所有成员(包括私有成员、保护成员和公有成员)。每个对象都有自己的成员函数副本。普通成员函数必须通过对象来调用。
- 本质上是一个包含指向具体对象
this
指针的普通函数,即C++类的普通成员函数都隐式包含一个指向当前对象的this
指针。
- 本质上是一个包含指向具体对象
- 类的静态成员函数:类的静态成员函数属于类本身,而不属于类的任何对象。它只能访问类的静态成员变量和静态成员函数,不能访问类的非静态成员变量和非静态成员函数。静态成员函数可以通过类名直接调用,也可以通过对象调用。
- 静态成员函数在某种程度上类似于全局函数,因为它们不依赖于类的任何特定对象,而是属于类本身。这意味着你可以在不创建类的对象的情况下调用静态成员函数。
- 然而,静态成员函数并不完全等同于全局函数。静态成员函数仍然是类的一部分,它可以访问类的静态成员(包括私有静态成员),而全局函数则不能。
- 静态成员函数没有
this
指针。this
指针是一个指向调用成员函数的特定对象的指针。因为静态成员函数不依赖于任何特定对象,所以它没有this
指针。这也意味着静态成员函数不能访问类的非静态成员变量或非静态成员函数。
如果成员函数想作为回调函数来使用,如创建线程等,一般只能将它定义为静态成员函数才行。
在C++中,回调函数通常需要是全局函数或静态成员函数,因为它们具有固定的函数签名,可以被用作函数指针。非静态成员函数不能直接用作回调函数,因为它们有一个隐含的
this
参数,这会改变它们的函数签名。然而,有一些方法可以让你使用非静态成员函数作为回调函数。例如,你可以使用
std::bind
或lambda
表达式来捕获this
指针,然后调用非静态成员函数。这种方法在C++11及以后的版本中可用。
以下是一个简单的例子来说明这三种函数的区别:
1 |
|
类的构造函数和析构函数的调用时机
在C++中,类的构造函数和析构函数的调用时机如下:
-
构造函数:当创建类的对象时,会自动调用类的构造函数。构造函数的主要任务是初始化对象的数据成员。构造函数的名称与类名相同,没有返回类型。构造函数可以有参数,可以被重载。
- 当创建一个类的对象时,例如
MyClass obj;
,会调用默认构造函数(无参数的构造函数)。 - 当以参数方式创建一个类的对象时,例如
MyClass obj(1, 2);
,会调用相应的参数构造函数。 - 当一个对象作为另一个对象的初始化参数时,例如
MyClass obj1 = obj2;
或MyClass obj1(obj2);
,会调用拷贝构造函数。
- 当创建一个类的对象时,例如
-
析构函数:在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 |
|
在这个例子中,当MyClass obj;
执行时,会调用MyClass
的构造函数。当obj
离开其作用域(即main
函数结束时),会调用MyClass
的析构函数。
输出函数的选择
-
C++ 中
printf
和cout
有什么区别?在 C++ 中,
printf
和cout
都用于输出,但它们来自不同的库,有不同的用法和特性。下面是一些主要的不同之处:- 来源:
printf
是 C 语言中的标准输入输出函数,来源于 C 语言的标准库(stdio.h)。在 C++ 中依然可用,但需要包含头文件<cstdio>
或者 C 风格的<stdio.h>
。cout
是 C++ 专有的,属于 C++ 标准库中的一个对象,用于输出。cout
是定义在<iostream>
头文件中的标准输出流对象。
- 使用语法:
printf
使用格式化字符串。例如:printf("%d %s", 100, "C++");
。cout
使用流插入运算符(<<
)。例如:cout << 100 << " C++";
。
- 类型安全:
printf
在处理类型时不够安全,因为它依赖于格式化字符串正确地匹配变量的类型;如果不匹配,可能会造成运行时错误或未定义行为。cout
是类型安全的,编译器会检查插入到cout
中的数据类型,确保与预期匹配,减少了错误的可能性。
- 性能:
printf
的性能通常情况下比cout
快,尤其是在格式化字符串操作时,因为cout
的类型安全和重载运算符使得它的运行效率稍低。- 但是,通过启用 C++ 的
std::ios_base::sync_with_stdio(false);
以及避开使用std::endl
(std::endl
会刷新输出缓冲区),可以提升cout
的性能,使其与printf
相近。
- 灵活性和功能:
- 使用
cout
更灵活,因为它支持对输出格式的更细致控制,例如设置宽度、精度和填充字符等。此外,cout
可以直接打印用户定义的类型,只要为这些类型重载了输出运算符(<<
)。 printf
在打印自定义类型时没有直接的方式,因为它需要预定义的格式化串。
- 使用
- 可移植性:
printf
基于 C 语言,因此在许多不同的平台和编译器中使用广泛,具有很高的可移植性。cout
虽然是 C++ 的一部分,理论上在所有支持 C++ 的平台上都是可用的,但在跨平台编程时,对输出格式的控制可能会遇到不一致性。
综上所述,
printf
和cout
都有各自的用途和优势,选择使用哪个取决于具体的编程需求和个人喜好。在实际编程中,了解它们的差异有助于更好地决策和优化代码。 - 来源:
-
在C++程序开发中,推荐使用
printf
还是cout
?在 C++ 程序开发中,通常推荐使用
cout
而不是printf
。这一推荐基于以下几个理由:- 类型安全:
cout
是类型安全的,这意味着编译器会对插入到cout
的数据类型进行检查,减少了类型不匹配的错误。而printf
需要开发者手动指定类型,在类型不匹配的情况下可能导致未定义行为或运行时错误。 - 易于使用:使用
cout
需要较少的手工干预来处理不同类型的数据,因为它可以自动调整输出格式,而printf
请求用户显式地指定格式化字符串。特别是在处理字符串和数字的混合输出时,cout
的语法更为直观和简洁。 - 可扩展性:对于自定义类型,
cout
允许开发者通过重载输出运算符 (<<
) 来定义自己的输出格式,这提供了良好的可扩展性。而printf
不能直接支持自定义类型的打印,除非通过某种方式将其转换为内建类型。 - 标准一致性:
cout
是 C++ 标准库的一部分,充分利用cout
能够使程序更加面向对象,更符合 C++ 的编程风格。printf
来自 C 语言标准库,虽然在 C++ 中仍然可用,但并不是 C++ 风格的首选。 - 灵活性:
cout
提供了更好的控制输出格式的功能,如设置宽度、填充、精度等,而且这些设置易于读写和维护。
然而,在某些特定情况下,
printf
可能更为有用: - 当您需要更详细地控制输出格式,尤其是在涉及复杂格式时,printf
的格式化字符串可能更易于使用和理解。 - 在性能极致敏感的场景中,经过适当的优化后,printf
在某些情况下可能比cout
有更好的性能。总之,虽然
cout
通常是在 C++ 中处理输出的首选,但确切选择取决于具体的应用场景、性能需求和个人偏好。在决定使用哪一个之前,考虑项目的需求和上下文非常重要。 - 类型安全:
-
在使用C++语言的ROS平台开发中,推荐使用
printf
、cout
还是ROS_INFO
?在使用 C++ 语言开发 ROS (Robot Operating System) 平台的应用时,通常推荐使用
ROS_INFO
以及其相关的宏(如ROS_WARN
,ROS_ERROR
,ROS_DEBUG
等)来进行消息的输出。这些宏专为 ROS 开发设计,提供了一些优势和特性,特别是在进行机器人软件开发时:- 统一的日志管理:
ROS_INFO
和其他相关宏能够将输出消息统一管理,使得日志记录、查看和分析变得更加容易。ROS 提供的 rqt_console 和 roslaunch 日志文件都可以捕捉这些输出,方便开发者调试和追踪问题。 - 级别管理:不同于
printf
和cout
只能单纯输出文本,ROS_INFO
等宏允许你按照不同的严重性级别输出消息,如信息、警告、错误、调试等。这有助于开发者在运行时筛选和控制想要查看的输出类型。 - 节点识别:使用 ROS 日志函数,每条日志消息都会自动附带节点名信息。这对于在复杂系统中跟踪消息来源特别有用,尤其是当多个节点同时运行时。
- 格式化和类型安全:
ROS_INFO
等宏支持类似printf
的格式化功能,使得在输出文本时可以包含变量。同时,由于ROS_INFO
是在 C++ 环境下设计的,因此也可以在一定程度上享受到 C++ 类型安全的好处(尽管其内部实现可能是利用printf
风格的格式化字符串)。 - 条件编译:
ROS_DEBUG
等调试相关的宏,在发布模式下可以通过编译选项完全禁用,从而不会对发布版本的性能造成影响。
综上所述,虽然在 ROS 平台下开发时
printf
和cout
依然可用,但ROS_INFO
和相关的宏提供了更多符合 ROS 开发环境和需求的优势。因此,在大多数情况下,推荐使用ROS_INFO
及其相关宏进行消息输出和日志记录。 - 统一的日志管理:
-
在使用C++语言的ROS平台开发中,推荐使用
printf
、cout
、ROS_INFO
还是ROS_INFO_STREAM
?在使用C++语言的ROS(Robot Operating System)平台开发时,选择
printf
、cout
、ROS_INFO
还是ROS_INFO_STREAM
取决于具体的应用场景和你的偏好。下面是它们各自的特点和适用情况:printf
- 来源于C语言,C++也支持。
- 用于格式化输出,语法稍显复杂,对类型安全的支持不如C++的IO流。
- 适用于快速的调试或者当开发环境更接近C语言时使用。
std::cout
- C++标准输入输出流的一部分,使用起来比
printf
更为方便和安全。 - 支持类型安全的输出,可以通过重载
<<
操作符来扩展对自定义类型的支持。 - 适用于C++程序的通用输出,特别是需要输出自定义类型数据时。
ROS_INFO
和ROS_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::cout
和ROS_INFO_STREAM
可以更好地利用C的特性。
综上所述,选择哪一种取决于你的具体需求和开发习惯。在ROS开发中,
ROS_INFO
和ROS_INFO_STREAM
因为其与ROS系统的集成,通常会是首选。
C++小技巧
模板参数必须在编译时是已知的常量
在C++中,模板参数必须在编译时是已知的常量。这意味着你不能使用运行时从配置文件中读取的值作为模板参数。
一种可能的解决方案是使用预处理器宏来定义N_dim
。你可以在编译时通过编译器的命令行参数来设置这个宏。例如:
1 |
|
预处理器宏(如
#define N 512
)在它被定义的文件以及#include
该文件的所有文件中都是可见的。如果你在一个文件中定义了N
,然后在另一个文件中想要使用它,你需要确保第二个文件#include
了定义N
的文件。
然后在编译时,你可以使用-D
选项来设置N_dim
的值:
1 |
|
另一种解决方案是使用一个固定大小的eFFT
对象,然后在运行时根据需要使用其中的一部分。这可能需要你修改eFFT
类的实现,使其能够处理不同大小的输入。
重复定义问题
你的问题是在多个源文件中都包含了parameters.h
,并且在这个头文件中定义了eFFT_SIZE
。这导致了eFFT_SIZE
的多重定义。
解决这个问题的一种方法是在parameters.h
中只声明eFFT_SIZE
,然后在一个源文件(例如parameters.cpp
)中定义它。这样,eFFT_SIZE
就只在一个地方定义了。
首先,你需要在parameters.h
中将eFFT_SIZE
的定义改为声明:
1 |
|
然后,在parameters.cpp
中定义eFFT_SIZE
:
1 |
|
这样,eFFT_SIZE
就只在parameters.cpp
中定义了一次,而在其他源文件中,它只是一个外部链接的声明。这应该可以解决你的问题。d
函数多个返回值
在C++中,有几种方法可以实现函数的多个返回值:
-
使用引用参数(Reference Parameters)/指针:你可以通过引用参数来修改函数外部的变量,从而实现多个返回值。
1
2
3
4
5
6
7
8
9
10void getValues(int& x, int& y) {
x = 5;
y = 10;
}
int main() {
int a, b;
getValues(a, b);
// Now a == 5 and b == 10
} -
使用std::pair或std::tuple:如果你的函数需要返回两个或更多的值,你可以使用
std::pair
或std::tuple
。1
2
3
4
5
6
7
8std::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
} -
使用结构体(Structs)或类(Classes):如果你的函数需要返回多个相关的值,你可以定义一个结构体或类来存储这些值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16struct 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
} -
使用数组。
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;
} -
选择哪种方法取决于你的具体需求和编程风格。
多个源文件中共享一个变量/全局变量
在C++中,如果你想在多个源文件中共享一个变量,你可以使用extern
关键字。你可以在一个源文件中定义一个变量,并在其他源文件中使用extern
关键字声明同一个变量。
首先,在ESVIO/pose_graph/src/pose_graph_node.cpp
文件中,你需要将SYSTEM_MODE
变量的定义移动到一个头文件中,例如global.h
。然后在pose_graph_node.cpp
和keyframe.cpp
中都包含这个头文件。
global.h
文件内容如下:
1 |
|
然后在ESVIO/pose_graph/src/pose_graph_node.cpp
文件中,你需要包含global.h
并定义SYSTEM_MODE
:
1 |
|
最后,在ESVIO/pose_graph/src/keyframe.cpp
文件中,你也需要包含global.h
。这样你就可以在keyframe.cpp
中使用SYSTEM_MODE
变量了:
1 |
|
注意,extern
关键字告诉编译器变量的定义在别的地方,你不能在声明的时候初始化它。变量的定义(也就是初始化)应该在一个源文件中进行,而不是头文件。如果你在多个地方定义了同一个extern
变量,那么会导致链接错误。
程序计时
1 |
|
1 |
|
类与对象
为什么要定义类与对象,什么时候定义类与对象?
在面向对象的编程语言中,类(Class)是一种定义新类型的方式,它封装了数据和操作数据的方法。对象(Object)则是类的实例,它包含了类定义的数据和方法。
定义类和对象的目的主要有以下几点:
- 封装:类可以将数据和操作数据的方法封装在一起,隐藏内部实现细节,提高代码的安全性和可维护性。
- 继承:类可以继承其他类的属性和方法,实现代码的复用。
- 多态:通过接口或者虚函数,不同的类对象可以执行相同的操作,但表现出不同的行为,提高代码的灵活性。
何时定义类和对象主要取决于你的需求。以下是一些可能需要定义类和对象的情况:
- 当你需要创建一个复杂的数据结构,这个数据结构包含多个相关的数据和操作这些数据的方法时,你可以定义一个类来封装这些数据和方法。
- 当你需要创建多个具有相同属性和行为的对象时,你可以定义一个类,然后创建这个类的多个对象。
- 当你需要实现面向对象的设计模式(如工厂模式、单例模式等)时,你需要定义类和对象。
怎么用类与对象
尽量实现,提供类所需要的输入数据,所有操作在类内部封装和实现,在外部直接输出结果。
这个想法是面向对象编程(OOP)的核心原则之一,称为封装(Encapsulation)。封装是将数据(变量)和操作数据的函数(方法)捆绑在一起形成一个对象(即类的实例),并规定了外部对这些数据和函数的访问方式。
封装有以下几个优点:
- 提高安全性:通过隐藏对象的内部状态,只允许通过对象的方法来访问和修改,可以防止外部代码随意修改对象的内部状态,提高了代码的安全性。
- 简化接口:对象的用户只需要知道对象提供了哪些方法,而不需要知道这些方法是如何实现的。这使得代码更易于理解和使用。
- 提高可维护性:由于对象的内部实现被封装起来,所以在不影响对象的用户的情况下,可以更容易地改变对象的内部实现。
面向对象编程(OOP)的核心原则主要有以下四个:
- 封装(Encapsulation):封装是将对象的状态(属性)和行为(方法)捆绑在一起,同时隐藏对象的内部实现细节,只提供有限的接口供外部访问。这样可以保护对象的内部状态,提高代码的安全性和可维护性。
- 继承(Inheritance):继承是子类可以继承父类的属性和方法,实现代码的复用。子类可以扩展和修改父类的行为,提供更具体的功能。
- 多态(Polymorphism):多态是指不同的对象可以对同一消息做出不同的响应。在运行时,根据对象的实际类型来调用相应的方法,提高了代码的灵活性。
- 抽象(Abstraction):抽象是将复杂的系统简化为更简单的模型。通过定义抽象的类和接口,隐藏具体的实现细节,让程序员只关注有用的信息。
这四个原则是面向对象编程的基础,它们使得代码更易于理解、维护和扩展。
模板类
模板类是C++中一种特殊的类,它可以用于创建处理不同数据类型的类的蓝图。模板类的定义以关键字template
开始,后面跟一个或多个模板参数。
例如,你可以定义一个模板类Array
,它可以用于创建处理不同类型元素的数组:
1 |
|
在这个例子中,T
是一个类型模板参数,N
是一个非类型模板参数。你可以用任何类型替换T
,用任何整数替换N
,来创建不同的Array
类:
1 |
|
模板类与一般的类的主要区别在于,模板类可以处理多种类型的数据,而一般的类只能处理特定类型的数据。模板类提供了一种机制,使得你可以在类定义时不指定具体的类型,而是在使用类时指定类型。这使得你的代码更加灵活,可以处理多种类型的数据。
成员初始化列表
成员初始化列表(Member Initializer List)是C++中一个重要的特性,它允许在构造函数中初始化类的成员变量。这个特性不仅可以提高代码的清晰度和执行效率,还是某些情况下初始化成员变量的唯一方法。
在C++中,构造函数可以包含一个初始化列表,用于在进入构造函数体之前初始化成员变量。初始化列表以冒号(:)开头,后面跟着用逗号(,)分隔的初始化表达式。例如:
1 |
|
在这个例子中,*x_和y_*是通过成员初始化列表进行初始化的,而不是在构造函数体内赋值。
有几种情况下必须使用成员初始化列表:
- const成员变量:const成员变量必须在声明时初始化,不能在构造函数体内赋值。
- 引用成员变量:引用成员同样需要在声明时初始化。
- 没有默认构造函数的类类型成员:如果类成员本身是一个对象,并且这个对象没有无参的构造函数,那么必须通过成员初始化列表来调用其有参构造函数。
- 继承中的基类成员:如果需要在子类中初始化基类的私有成员,也需要在成员初始化列表中显式调用基类的构造函数。
使用成员初始化列表初始化类成员比在构造函数体内赋值有几个优势:
- 性能:对于类类型的成员变量,使用成员初始化列表可以避免调用默认构造函数后再赋值,减少了一次不必要的构造和析构过程,提高了效率。
- 顺序:成员变量的初始化顺序与它们在类定义中的声明顺序一致,与初始化列表中的顺序无关。这有助于避免一些因初始化顺序不当导致的错误。
1 |
|
在这个示例中,类A的成员a是一个const成员,类B的成员b是一个引用成员。它们都通过成员初始化列表进行了初始化。
其它
-
在C++中,如果一个类没有明确指定访问修饰符(
public
、protected
或private
),那么默认的访问级别是private
。这意味着,如果你没有在类定义中看到任何访问修饰符,那么该类的所有成员都是私有的。 -
初始赋值:
1
2
3
4
5class FeatureTracker
{
private:
int trackIDCounter_ = 1;
}trackIDCounter_ = 1;
是对成员变量trackIDCounter_
的初始赋值。这意味着,当创建这个类的对象时,trackIDCounter_
的初始值将被设置为1。在类的成员函数中,你可以更改这个成员变量的值。例如,你可以在一个成员函数中增加
trackIDCounter_
的值:1
2
3void FeatureTracker::incrementTrackID() {
trackIDCounter_++;
}在这个例子中,
incrementTrackID
函数将trackIDCounter_
的值增加1。你可以在类的任何成员函数中更改trackIDCounter_
的值,只要这个函数有权限访问这个成员变量(即,这个函数不是const
的)。 -
等等。
结构体
概述
定义一个结构体类型就类似于定义了一个变量类型,结构体的用法类似于变量,只不过一个结构体里可以包含多个变量类型。
结构体(struct
)在C++中是一种复合数据类型,它可以包含多个不同类型的数据成员。你可以将结构体看作是一个“自定义的数据类型”,这个数据类型可以包含多个其他的数据类型。
例如,你可以定义一个BoundingBox
结构体来表示一个目标检测框,这个结构体包含了x
、y
、w
、h
四个整型数据成员。然后,你就可以像使用其他数据类型一样使用这个BoundingBox
结构体,例如创建BoundingBox
类型的变量,将BoundingBox
类型的变量作为函数的参数,等等。
将一组相关的变量定义为结构体(struct
)有以下几个好处:
-
组织性:结构体可以将一组相关的数据组织在一起,使代码更加清晰和易于理解。
-
代码复用:你可以多次使用同一个结构体,这有助于减少代码重复。
如果你需要在其他地方使用相同的一组变量,你可能需要再次声明和初始化这些变量,这就是代码重复。但是,如果你将这些变量定义为一个结构体,你就可以在需要的地方创建这个结构体的实例,而不需要重复声明和初始化这些变量。这就是代码复用。以下是一个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20struct 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
的实例box1
和box2
,并将它们传递给processBoundingBox
函数进行处理。这样,我们就复用了BoundingBox
结构体的定义,而不需要在每个需要使用目标检测框的地方都声明和初始化x
、y
、w
、h
这四个变量。 -
易于维护:如果你需要修改这组数据,只需要在一个地方(即结构体定义)进行修改,而不是在代码的多个地方。
当你需要修改或更新代码时,结构体可以使这个过程更加简单和直接。举个例子,假设你有一个关于目标检测框的结构体:
1
2
3
4
5
6struct BoundingBox {
int x;
int y;
int w;
int h;
};现在,你想要添加一个新的属性,比如目标检测框的颜色。如果你没有使用结构体,你可能需要在代码的多个地方添加新的变量,并且需要确保这些变量在所有的函数和方法中都被正确地更新和使用。
但是,如果你使用了结构体,你只需要在结构体的定义中添加新的属性:
1
2
3
4
5
6
7struct BoundingBox {
int x;
int y;
int w;
int h;
std::string color;
};这样,所有使用
BoundingBox
的地方都会自动获得新的color
属性,你只需要在适当的地方更新和使用这个新的属性即可。这就是结构体使代码"易于维护"的一个例子。 -
封装:结构体可以封装数据和操作,使得数据和操作紧密相关,提高代码的可读性和可维护性。
使用
定义
在C++中,struct
可以包含各种类型的成员,包括基本类型(如int
、double
等)、类对象、数组、vector
等。以下是一个例子:
1 |
|
在这个例子中,MyStruct
包含一个int
类型的成员id
和一个vector<int>
类型的成员values
。
你可以定义一个结构体来表示单个目标检测框,然后使用vector
来存储多个这样的目标检测框。这样做的好处是,你可以很容易地添加、删除和遍历目标检测框,而且代码的可读性和可维护性也会提高。
1 |
|
在这个例子中,BoundingBox
是一个结构体,它表示一个目标检测框。boxes_left
和boxes_right
是两个vector
,它们分别存储左目和右目的目标检测框。
当你需要添加一个新的目标检测框时,你可以创建一个BoundingBox
的实例,设置它的属性,然后将它添加到boxes_left
或boxes_right
中。当你需要遍历所有的目标检测框时,你可以遍历boxes_left
或boxes_right
,并对每个BoundingBox
实例进行操作。
赋值与引用
在C++中,你可以通过.
操作符来引用struct
中的成员。如果你的struct
中有一个vector
成员,你可以像下面这样引用它:
1 |
|
在这个例子中,MyStruct
是一个结构体,它有一个vector<int>
类型的成员values
。在main
函数中,我们创建了一个MyStruct
类型的变量s
,然后通过.
操作符来访问和操作它的values
成员。
1 |
|
在这个例子中,我们首先创建了一个BoundingBox
的实例box
,然后设置了box
的属性,最后将box
添加到boxes_left
中。这个过程在循环中重复,直到处理完所有的目标检测框。
我们使用了boxes_left[i]
来访问boxes_left
中的第i
个元素,然后使用.
操作符来访问这个元素的数据成员。这个过程在循环中重复,直到处理完boxes_left
中的所有元素。
在C++中,你可以使用vector
的clear
方法来清空vector
中的所有元素。以下是一个例子:
1 |
|
在这个例子中,boxes_left.clear()
将清空boxes_left
中的所有元素。这个操作将使boxes_left
的大小变为0,但不会改变它的容量。如果你希望同时清空vector
的元素和容量,你可以使用swap
方法:
1 |
|
在这个例子中,vector<BoundingBox>().swap(boxes_left)
将创建一个新的空vector
,然后与boxes_left
交换。这个操作将使boxes_left
的大小和容量都变为0。
清空/初始化
在C++中,你可以使用构造函数或者赋值运算符来初始化或清空一个结构体的值。以下是一个例子:
1 |
|
在这个例子中,BoundingBox
结构体有一个构造函数,它将所有的数据成员初始化为0。clear
方法将所有的数据成员设置为0。在main
函数中,我们创建了一个BoundingBox
类型的变量box
,然后使用构造函数和clear
方法来初始化和清空box
的值。
作为函数的参数
在C++中,你可以将结构体作为函数的输入参数或输出参数。以下是一个例子:
1 |
|
在这个例子中,printPoint
函数接收一个Point
类型的参数,getPoint
函数返回一个Point
类型的值。在main
函数中,我们创建了两个Point
类型的变量p1
和p2
,并使用printPoint
函数打印它们的值。
定义结构体里的函数
https://www.runoob.com/cplusplus/cpp-struct.html
1 |
|
结构体与类的区别
在 C++ 中,struct 和 class 本质上非常相似,唯一的区别在于默认的访问权限:
struct
默认的成员和继承是public
。class
默认的成员和继承是private
。
你可以将 struct
当作一种简化形式的 class
,适合用于没有太多复杂功能的简单数据封装。
数组
定义
C++代码 int record[26] = {1};
的作用是:
- 声明一个数组:
int record[26]
声明了一个包含26个整数的数组,数组元素类型为int
。 - 初始化数组:
= {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 |
|
在这个示例中,sizeof(arr)
返回数组的总字节数,sizeof(arr[0])
返回单个元素的字节数。通过将总字节数除以单个元素的字节数,我们就可以得到数组的大小。
使用std::array
如果你在使用C++11及以上版本,推荐使用std::array
,它是一个封装了静态数组的模板类,并提供了一个可以直接获取大小的方法。以下是一个示例:
1 |
|
在这个示例中,arr.size()
直接返回数组的大小,使用起来非常方便。
使用std::vector
如果你需要一个动态大小的数组,推荐使用std::vector
,它是一个动态数组模板类。以下是一个示例:
1 |
|
在这个示例中,arr.size()
直接返回数组的大小,使用起来也非常方便。
总结
- 对于静态数组,使用
sizeof
运算符。 - 对于C++11及以上版本的静态数组,使用
std::array
。 - 对于动态数组,使用
std::vector
。
选择合适的方法可以使代码更加简洁和易于维护。
set
定义与使用
1 |
|
举例
1 |
|
map
定义和使用
1 |
|
1 |
|
vector
定义
1 |
|
添加元素
如果vector
变量的大小没有预先定义,你需要先调用resize
函数来设置它们的大小,然后再使用索引来赋值。或者,你可以使用push_back
或emplace_back
来添加元素。
以下是使用resize和索引赋值的示例:
1 |
|
以下是使用push_back
添加元素的示例:
1 |
|
如果你知道vector
的最终大小,那么使用resize
函数来预先设置大小,然后使用索引来赋值,通常会更快。这是因为resize
函数会一次性分配足够的内存,而不需要在每次添加元素时重新分配和复制内存。此外,使用索引来赋值可以直接访问和修改vector
中的元素,而不需要调用push_back
或emplace_back
函数。
如果你不知道vector
的最终大小,或者vector
的大小可能会动态变化,那么使用push_back
或emplace_back
来添加元素可能会更方便。这是因为push_back
和emplace_back
函数可以在不知道vector
最终大小的情况下动态添加元素。但是,你需要注意的是,如果vector
的大小增长很快,push_back
和emplace_back
函数可能需要多次重新分配和复制内存,这可能会影响性能。
总的来说,如果你知道vector
的最终大小,推荐使用resize
和索引赋值。如果你不知道vector
的最终大小,或者vector
的大小可能会动态变化,推荐使用push_back
或emplace_back
。
clear与容量
clear
在C++中,你可以使用vector
的clear
方法来清空vector
中的所有元素。以下是一个例子:
1 |
|
在这个例子中,curBoxesLeft_.clear()
将清空curBoxesLeft_
中的所有元素。这个操作将使curBoxesLeft_
的大小变为0,但不会改变它的容量。如果你希望同时清空vector
的元素和容量,你可以使用swap
方法:
1 |
|
在这个例子中,vector<BoundingBox>().swap(curBoxesLeft_)
将创建一个新的空vector
,然后与curBoxesLeft_
交换。这个操作将使curBoxesLeft_
的大小和容量都变为0。
然后,你可以使用push_back
或emplace_back
方法向curBoxesLeft_
添加新的目标检测框。例如:
1 |
|
在这个例子中,我们首先创建了一个新的BoundingBox
对象newBox
,然后将其添加到curBoxesLeft_
中。
在C++中,
vector
的clear
方法会移除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 |
|
在这个例子中,我们首先创建了一个空的vector
,然后添加了一个元素,然后预先分配了足够的内存来存储100个元素。你可以看到,vector
的大小和容量在这个过程中是如何变化的。
其它
std::vector
的赋值操作符(=
)执行的是内容的复制,而不是地址的复制。
string
使用
1 |
|
1 |
|
mutex
互斥量(mutex),它是一种同步原语,用于保护共享数据免受多个线程同时访问。在多线程环境中,如果多个线程试图同时访问和修改同一块数据,可能会导致数据不一致和未定义的行为。为了防止这种情况,我们可以使用互斥量来确保在任何时候只有一个线程能够访问该数据。
比如在回调函数中被持续赋值,在其它函数中被修改。
即使变量在其他函数中只被读取,也需要保护它,因为在多线程环境中,一个线程可能在另一个线程正在写入变量的同时读取该变量,这可能会导致读取到的数据是不一致或者无效的。这种情况被称为“读-写冲突”。
一个或多个共享资源(例如,一个全局变量或一个在多个线程之间共享的数据结构),当一个线程想要访问这个共享资源时,它需要首先锁定(lock)互斥量。如果互斥量已经被另一个线程锁定,那么这个线程将会被阻塞,直到互斥量被解锁(unlock)。当线程完成对共享资源的访问后,它需要解锁互斥量,以便其他线程可以锁定互斥量并访问共享资源。
lock
1 |
|
你需要手动调用
lock()
和unlock()
来锁定和解锁互斥量。这种方式的问题是,如果在lock()
和unlock()
之间的代码抛出了异常,那么unlock()
可能永远不会被调用,从而导致死锁。
示例:
1 |
|
lock_guard
在C++中,std::lock_guard
对象的作用域是由其所在的代码块(即最近的大括号{}
内的区域)决定的。当std::lock_guard
对象在代码块内创建时,它会自动锁定传递给它的互斥量。当std::lock_guard
对象超出其作用域(即离开其所在的代码块)时,它的析构函数会被调用,从而自动解锁互斥量。
1 |
|
使用了
std::lock_guard
,这是一个RAII(Resource Acquisition Is Initialization)机制的互斥包装器,它在构造时提供一个已锁定的互斥,并在析构时解锁互斥。这意味着当std::lock_guard
对象超出其作用域并被销毁时,互斥量会自动被解锁,即使在lock_guard
的作用域内的代码抛出了异常。这样可以避免死锁,并使代码更安全、更易于理解。
示例:
1 |
|
Eigen
Matrix类与Array类
相对于Matrix类提供的线性代数(矩阵)运算,Array类提供了更为一般的数组功能。Array类为元素级的操作提供了有效途径,比如点加(每个元素加值)或两个数据相应元素的点乘。
1 |
|
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 |
|
在这个例子中,我们创建了一个3x3的矩阵,所有元素的初始值都是-1,然后我们将第一行第一列的元素值设置为-5。
当你使用OpenCV的imshow
函数来可视化一个cv::Mat
矩阵时,负值的处理方式取决于矩阵的数据类型。
如果你的cv::Mat
矩阵的数据类型是无符号整数(如CV_8U
),那么它不能存储负值,任何负值都会被视为零。
如果你的cv::Mat
矩阵的数据类型是有符号整数(如CV_8S
,CV_16S
)或浮点数(如CV_32F
,CV_64F
),那么它可以存储负值。在可视化这样的矩阵时,你需要先将矩阵的值规范化到0-255的范围内。你可以使用OpenCV的normalize
函数来实现这一点。
以下是一个例子:
1 |
|
在这个例子中,我们首先创建了一个3x3的矩阵,所有元素的初始值都是-1。然后我们使用cv::normalize
函数将矩阵的值规范化到0-255的范围内,并将数据类型转换为CV_8U
。最后,我们使用cv::imshow
函数显示规范化后的图像。
访问cv::Mat
对象中特定位置的像素值
在OpenCV中,有几种方法可以访问cv::Mat
对象中特定位置的像素值。以下是一些常见的方法:
-
使用
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
的数据。 -
使用
ptr
函数:这个函数返回一个指向图像某一行的指针,然后你可以像操作普通数组一样操作这个指针。这个方法通常比at
函数快,但是也更容易出错,因为你需要自己管理指针。例如:1
2unsigned char* row_ptr = cur_event_mat_left_fft.ptr<unsigned char>(e_left.y);
unsigned char pixel_value = row_ptr[e_left.x]; -
使用迭代器:你也可以使用C++的迭代器来访问
cv::Mat
中的像素。这个方法比较安全,但是通常比at
和ptr
函数慢。例如:1
2cv::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
对象A
和B
,并且你执行了A = B;
,那么A
和B
将共享相同的数据。如果你修改了A
中的数据,B
中的数据也会被修改。
这是因为cv::Mat
使用了引用计数机制来管理数据。当你创建一个新的cv::Mat
对象并赋值给另一个cv::Mat
对象时,它们都会指向同一个数据,而且这个数据的引用计数会增加。当一个cv::Mat
对象被销毁时,它会减少数据的引用计数。只有当引用计数变为0时,数据才会被释放。
如果你希望创建一个cv::Mat
的真正副本,你可以使用clone
或copyTo
方法。例如,cv::Mat B = A.clone();
将创建一个新的cv::Mat
对象B
,它包含了A
的一个副本。在这种情况下,A
和B
不会共享数据。
clone和copyTo
在OpenCV中,clone
和copyTo
函数都可以用来复制cv::Mat
对象,但它们的使用方式和行为有一些不同。
clone
函数创建一个新的cv::Mat
对象,并复制源对象的所有数据。它不需要一个已经存在的目标对象,因为它会创建一个新的对象。例如:
1 |
|
copyTo
函数将源对象的数据复制到目标对象。如果目标对象已经存在,它的大小和类型必须与源对象匹配,否则它会被重新分配。copyTo
还有一个可选的参数,允许你指定一个掩码,只有掩码中非零的元素才会被复制。例如:
1 |
|
总的来说,如果你只需要复制一个cv::Mat
对象,并且不需要使用掩码,那么clone
可能是更简单的选择。如果你需要使用掩码,或者你已经有一个目标对象并希望复制数据到这个对象,那么copyTo
可能是更好的选择。
组织管理一组变量或函数
在C++中,除了结构体(struct
),还有以下几种方式可以组织和管理一组变量或函数:
- 类(Class):类是C++中的一个核心概念,它可以包含变量(称为成员变量)和函数(称为成员函数)。类提供了封装、继承和多态等面向对象编程的特性。
- 命名空间(Namespace):命名空间可以用来组织一组相关的变量和函数,以避免命名冲突。
- 数组(Array)和向量(Vector):如果你有一组相同类型的变量,你可以使用数组或向量来存储它们。
- 函数(Function):如果你有一组相关的操作,你可以将它们封装在一个函数中。
- 枚举(Enum):如果你有一组相关的常量,你可以使用枚举来定义它们。
- 联合(Union):联合是一种特殊的数据类型,它可以存储不同类型的数据,但一次只能存储其中一种类型的数据。
选择哪种方式取决于你的具体需求和使用场景。
Python
if name == "main":
在Python中,if __name__ == "__main__":
是一个常见的模式。这行代码的作用是检查当前的模块是被直接运行还是被导入为一个模块。
当Python解释器读取一个源文件时,它会首先定义一些特殊的变量。其中一个就是 __name__
。如果该文件被直接运行,那么 __name__
的值会被设置为 "__main__"
。如果该文件被其他Python文件导入,那么 __name__
的值则会被设置为该文件的名字。
因此,if __name__ == "__main__":
这行代码的意思是,"如果这个文件被直接运行,那么执行以下的代码"。这个模式常常被用来在一个Python文件中编写一些测试代码,这些测试代码只有在文件被直接运行时才会执行,而在文件被导入时不会执行。
读取文件
1 |
|
在Python中,使用with
语句打开文件时,当with
语句的代码块执行完毕后,文件会自动关闭。所以,你不需要显式地调用file.close
。
这是因为with
语句创建了一个上下文,当离开这个上下文时,Python会自动清理相关的资源。在这个例子中,相关的资源就是打开的文件。
1 |
|
在data = yaml.safe_load(file)
这行代码执行完毕后,file
会自动关闭,无需手动关闭。
读取参数
1 |
|
1 |
|
初始化列表
用法
在C++中,初始化列表用于在构造函数中初始化类的成员变量。它允许在进入构造函数体之前对成员变量进行初始化,从而提高效率和简洁性。以下是初始化列表的语法和用法:
1 |
|
在这个例子中,MyClass
有两个成员变量x
和y
,它们在构造函数的初始化列表中被初始化为a
和b
的值。
初始化列表的优点包括:
- 提高性能:避免了成员变量的默认初始化和随后赋值的开销。
- 初始化常量成员:可以初始化
const
成员和引用成员。 - 初始化基类:可以在派生类的初始化列表中调用基类的构造函数。
其它用法
除了类,C++中的初始化列表还可以用于以下场景:
-
结构体
1
2
3
4
5
6
7
8
9
10
11struct 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
2std::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
10class 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
3for (int i : {1, 2, 3, 4, 5}) {
// Do something with i
}
这些示例展示了初始化列表在C++中的广泛应用,不仅限于类和结构体。
内存泄漏
什么是内存泄漏?
内存泄漏是指程序中已分配的内存未能(在离开其作用域、程序运行结束后)成功释放,导致可用内存逐渐减少的现象。在程序运行过程中,如果反复发生内存泄漏,最终可能会导致系统可用内存耗尽,从而影响程序的性能或导致程序崩溃。内存泄漏在长时间运行的程序中尤其危险,例如服务器或持续运行的后台任务。
内存泄漏的原因
内存泄漏通常发生在以下几种情况:
-
未释放动态分配的内存:当使用如
malloc
,calloc
,realloc
和new
等函数分配内存后,未使用对应的 free 或 delete 来释放内存。 -
资源占用:除了内存外,程序可能申请其他系统资源(如文件句柄、数据库连接等),未正确释放这些资源也会导致类似内存泄漏的问题。
-
数据结构错误:例如,链表、树等数据结构若未正确处理其元素的删除操作,可能导致部分节点成为不可达的,从而造成内存泄漏。
如何判断内存泄漏?
判断和诊断内存泄漏通常需要以下几个步骤或工具:
- 代码审查:通过审查代码来寻找可能未释放内存的地方。特别关注那些有动态内存分配的函数或模块。
- 运行时工具:
- Valgrind:这是一个编程工具,用于内存调试、内存泄漏检测等。在 Linux 环境下,使用 Valgrind 运行程序可以帮助检测内存泄漏。
- Visual Studio:在 Windows 环境下,Visual Studio IDE 提供了内置的内存泄漏检测工具。
- Sanitizers:如 AddressSanitizer,这是一种快速的内存错误检测工具,可以集成到 GCC 或 Clang 编译器中,用于检测内存泄漏和其他内存相关错误。
- 性能监控工具:使用系统或第三方性能监控工具来观察程序的内存使用情况,查看内存使用是否随时间持续增加。
- 日志和追踪:在代码中添加日志输出,特别是在分配和释放资源的地方,可以帮助追踪内存的使用和释放。
如何防止内存泄漏?
- 使用智能指针:在 C++中使用
std::unique_ptr
,std::shared_ptr
等智能指针可以自动管理内存,大大减少内存泄漏的风险。 - 资源获取即初始化(RAII, Resource Acquisition Is Initialization):这是一种编程范式。资源的获取即是初始化,资源的释放即是销毁。确保在对象的生命周期内资源被正确管理。通过在对象的构造函数中分配资源,并在析构函数中释放资源,可以保证资源总是被正确管理。
- 定期代码审查:定期进行代码审查可以帮助识别潜在的内存泄漏问题。
- 自动化测试:编写测试用例,尤其是针对资源管理的单元测试,可以在开发过程中早期发现和解决内存泄漏问题。
溢出
1 |
|
在这段代码中,nums[i]
和 nums[j]
是两个整数(假设它们是 int
类型)。nums[i] + nums[j]
表达式会先执行整数相加操作,然后将结果转换为 long
类型并赋值给 goal
。如果 nums[i] + nums[j]
的结果超过了 int
类型的范围(即发生溢出),则在转换为 long
类型之前已经丢失了信息,因此可能会导致错误的结果。
1 |
|
在这段代码中,(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 |
|
当一个
long
类型的变量与一个int
类型的变量相加时,int
类型的变量会被自动提升为long
类型,然后进行相加运算。这样可以确保运算结果的正确性和避免潜在的溢出问题。
MATLAB转C++
MATLAB做完之后,只要自己封装的函数参数足够简介,用自动生成的C++代码足够应用在工程了,与手写没什么区别。
是滴,我现在很多函数都是MATLAB写好然后转化成C代码直接调用,但是这种调用每次程序启动时会比较慢,你有什么解决的方法吗?
我感觉还好,可以试试指定变量类型,能用整数就不用double转换,能定点就改定点,如果是编译成库,尽量静态,是不是会好一点?
得分情况,如果程序里有涉及大矩阵运算,或者是信号处理的程序,MATLAB肯定比咱们自己写的C快,但如果涉及大量复杂循环过程,且很多过程没法矩阵化,那MATLAB速度确实比不上C。不过对于快速验证来说,MATLAB要比C方便很多。
请问MATLAB转C或C++有啥快速方法吗?还是需要一句一句转?
有一条指令,在MATLAB的command里面输入
1 |
|
具体可以网上搜一下。
静态代码分析
想问一下,有没有能够获取C++中各个方法之间的调用依赖关系的开源软件?
可以尝试Understand。
可以调试的话,直接GDB断点,明明白白。
Source Insight?没Understand好用。
其它
-
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. -
Python 并没有强制要求你用
Tab
缩进或者用空格缩进,但在PEP8中,建议使用4个空格来缩进。对于任何一个编辑器或者IDE,一般都有配置选项,可以设置把TAB
键展开为4个空格,以保证代码的兼容性。 -
命令行使用
\
实现换行:1
2
3sudo apt-get install \
ros-$1-sophus \
ros-$1-pcl-ros -
报错:
SyntaxError: Non-ASCII character '\xe5'
原因:Python默认是以ASCII作为编码方式的,如果在自己的Python源码中包含了中文(或者其他非英语系的语言),此时即使你把自己编写的Python源文件以UTF-8格式保存了,但实际上,这依然是不行的。
解决:在源代码的第一行加入:
1
# -*- coding: UTF-8 -*-
-
python引入本地字体:
1
2
3
4
5
6
7
8
9
10
11import 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') -
idea错误提示File was loaded in the wrong encoding: ‘UTF-8‘解决方法:
- 打开乱码文件,在软件右下角将当前页面的编码格式改为GB2312,弹出的提示消息中选择Reload;
- 在软件右下角将当前页面的编码格式改为utf-8,弹出的提示消息中选择Convert;
- 参考链接
-
如果你不想运行
predict.py
文件的main
函数第136行以下的代码,你可以使用Python的return
语句来提前结束函数的执行。你需要找到第136行的代码,并在其前面添加return
语句。这将导致函数在执行到这一行时立即返回,不再执行后续的代码。在循环语句里,可以使用
continue
。 -
syntax error near unexpected token '$'{\r''
字面意思上看是换行符出现问题,怀疑是Win下编辑过。
1
2
3# 用vim -b 查看,发现每一行多了~M
# 解决方法:
sed -i 's/\r//g' xxx.sh -
变量的定义和使用只在最近的
{}
内((主)函数、if
、for
等)适用。如果想要拓展变量的使用范围,可以在更外处的{}
内定义变量(然后在别处赋值);或者,声明为全局变量。 -
在C++中,函数的默认参数值通常在函数声明中给出,也就是在
.h
头文件中(而不用在函数定义的*.cpp
文件中再给出默认值)。这样,任何包含这个头文件的代码都可以看到这些默认值,并且可以选择是否提供自己的参数值。 -
一般来说,如果有 3 个或更多
if-else
分支,则应该考虑使用switch
。如果有10个或更多条件分支,您应该考虑使用config
变量或文件,并为该config
编写特定的函数来进行映射。如果映射逻辑复杂但使用频繁,可以考虑创建专用的规则引擎或DSL来处理。if
语句和switch
语句的选择:多个等值判断选择switch
,其它情况(区间范围等)选择if
。 -
在C++中,
i++
和++i
都会增加i
的值,但它们的主要区别在于它们的返回值和执行顺序。i++
是后置递增运算符。它首先返回i
的当前值,然后再将i
的值增加1。例如:1
2int i = 5;
int j = i++; // j现在是5,i现在是6++i
是前置递增运算符。它首先将i
的值增加1,然后返回新的i
值。例如:1
2int i = 5;
int j = ++i; // j现在是6,i现在也是6 -
在C++中,
#include <filename>
和#include "filename"
的主要区别在于编译器搜索头文件的方式。#include <filename>
:编译器在标准库路径中搜索头文件。这通常用于包含标准库的头文件,如<iostream>
、<vector>
、<mutex>
等。#include "filename"
:编译器首先在当前文件的目录中搜索头文件,如果在当前目录中找不到,编译器会在标准库路径中搜索。这通常用于包含用户自定义的头文件。
-
variable
的生命周期取决于它在哪里声明和定义。如果它在while
循环外部定义,那么它将在包含该循环的函数或作用域结束时被销毁。如果它在while
循环内部定义,那么它将在每次循环结束时被销毁,并在下一次循环开始时重新创建。 -
把函数的输入参数行想成是对输入参数的定义,把函数的调用行想成是对输入参数的赋值,函数内部就是变量赋值后的进一步操作。
-
变量
l
(包括函数的输入参数)被定义为const
,这意味着它的值在初始化后不能被修改。这是一种良好的编程实践,可以防止在后续的代码中意外修改l
的值。然而,如果你确定在后续的代码中不会修改
l
的值,那么将l
定义为const
并不是必须的。在这种情况下,将l
定义为const
主要是为了提高代码的可读性和可维护性,它可以让其他阅读你代码的人知道l
的值在初始化后不应该被修改。总的来说,是否将
l
定义为const
取决于你的编程风格和项目的编码规范。如果你的项目鼓励使用const
来增强代码的可读性和可维护性,那么将l
定义为const
是一个好的选择。 -
命令行参数:
1
2
3
4import 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]
是第二个参数,以此类推。 -
初始化 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"
-
i++
是先赋值,然后再自增;++i
是先自增,后赋值。 -
C++ 中 for 循环的语法:
1
2
3
4
5for ( init; condition; increment )
{
statement(s);
}下面是 for 循环的控制流:
- init 会首先被执行,且只会执行一次。这一步允许您声明并初始化任何循环控制变量。您也可以不在这里写任何语句,只要有一个分号出现即可。
- 接下来,会判断 condition。如果为真,则执行循环主体。如果为假,则不执行循环主体,且控制流会跳转到紧接着 for 循环的下一条语句。
- 在执行完 for 循环主体后,控制流会跳回上面的 increment 语句。该语句允许您更新循环控制变量。该语句可以留空,只要在条件后有一个分号出现即可。
- 条件再次被判断。如果为真,则执行循环,这个过程会不断重复(循环主体,然后增加步值,再然后重新判断条件)。在条件变为假时,for 循环终止。
-
针对多组输入输出且数据没有固定数据量我们通常这样解决问题:采用
while(scanf("%d",&n) != EOF)
或while(~scanf(“%d”, &n))
。EOF全称是End Of File(C语言标准函数库中表示文件结束符)。 -
在C++编程中,推荐在每个
for
循环中直接定义和初始化变量i
,即for(int i = 0; i < n; i++)
。这样做的好处包括:- 作用域管理:每个
i
变量的作用域仅限于对应的for
循环内部,避免了不同循环之间的命名冲突。 - 代码可读性:更容易理解和维护代码,因为变量
i
的作用范围明确。
- 作用域管理:每个
-
变量定义位置两种写法各有优缺点:
- 写法一:
tmp
和tmp1
变量在每次循环开始时定义,作用域仅限于循环体内。这样可以减少变量的生命周期,降低潜在的错误风险。 - 写法二:
tmp
和tmp1
变量在循环外定义,作用域覆盖整个循环。这样可以减少变量定义的次数,可能在性能上略有提升。
一般来说,推荐使用写法一,因为它使变量的作用域更清晰,减少了意外修改变量的风险。
- 写法一:
-
在C++中,
NULL
和nullptr
用于表示空指针,但它们有一些区别:NULL
:传统的C风格的空指针常量,通常定义为整数类型的0。nullptr
:C++11引入的新关键字,专门用于表示空指针,类型是std::nullptr_t
。
nullptr
更安全和明确,因为它专门用于指针,而NULL
是一个整数常量,可能导致类型不匹配的问题。 -
在C++中,
int* p
和int *p
两种写法都是有效的,表示相同的含义,即p
是一个指向int
类型的指针。选择哪种写法主要是风格问题:int* p;
:强调p
是一个指针类型。int *p;
:强调*p
是一个int
类型的变量。
两种写法都被广泛使用,选择哪种主要取决于个人或团队的编码规范。
-
在C++中,
new
关键字用于在堆上动态分配内存,并调用相应的构造函数来初始化对象。以下是new
关键字的用法:new
关键字通常用于动态分配内存并返回指向该内存的指针变量。这样可以在运行时灵活地管理内存,用于创建对象或数组。-
分配单个对象:
1
2int* p = new int; // 分配一个int类型的对象
*p = 5; -
分配并初始化单个对象:
1
int* p = new int(5); // 分配并初始化一个int类型的对象
-
分配数组:
1
int* arr = new int[10]; // 分配一个包含10个int类型元素的数组
-
分配自定义类型的对象:
1
2
3
4
5struct MyStruct {
int x;
MyStruct(int val) : x(val) {}
};
MyStruct* obj = new MyStruct(10); // 分配并初始化MyStruct类型的对象
记得在使用完动态分配的内存后使用
delete
或delete[]
释放它,以避免内存泄漏。 -
-
在C++中,关于变量声明的编程规范可能因团队或项目的编码风格指南而异。一般来说:
int i, j;
一行中声明多个变量,节省空间,适合声明类型相同且相关的变量。int i; int j;
每行声明一个变量,清晰明了,易于维护和调试。
很多编码规范推荐每行声明一个变量,以提高代码的可读性和可维护性。
-
begin()
和end()
是C++标准库容器类(如vector
,array
,list
,unordered_set
等)提供的成员函数,用于获取容器的迭代器。-
begin()
返回一个指向容器中第一个元素的迭代器。如果容器为空,begin()
返回的迭代器等于end()
。 -
end()
返回一个指向容器末尾后一个位置的迭代器(即一个“哨兵”位置,不指向任何有效元素)。end()
迭代器通常用于在循环中判断是否已经到达容器的末尾。
-
-
路径中有空格:
1
2
3~/Documents/test file/file_1
~/Documents/'test file'/file_1
~/Documents/test\ file/file_1 -
cd ${rosbag_dir}
还是cd "${rosbag_dir}"
更好些?在处理路径时,使用引号可以避免由于路径中包含空格或特殊字符而导致的问题。因此,
cd "${rosbag_dir}"
更好些。 -
for循环,多个循环变量:
1
2
3
4
5void reverseString(vector<char>& s) {
for (int i = 0, j = s.size() - 1; i < s.size()/2; i++, j--) {
swap(s[i],s[j]);
}
} -
等等。