编程知识点

本文最后更新于 2025年2月19日 中午

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

命名空间

是否使用命名空间?

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

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

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

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

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++中,普通函数、类的普通成员函数和类的静态成员函数之间有以下几点主要区别:

  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消息打印

  5. 等等。

运算符

C++ 运算符

算术运算符

假设变量 A 的值为 10,变量 B 的值为 20,则:

运算符 描述 实例
+ 把两个操作数相加 A + B 将得到 30
- 从第一个操作数中减去第二个操作数 A - B 将得到 -10
* 把两个操作数相乘 A * B 将得到 200
/ 分子除以分母 B / A 将得到 2
% 取模运算符,整除后的余数 B % A 将得到 0
++ 自增运算符,整数值增加 1 A++ 将得到 11
-- 自减运算符,整数值减少 1 A-- 将得到 9

关系运算符

假设变量 A 的值为 10,变量 B 的值为 20,则:

运算符 描述 实例
== 检查两个操作数的值是否相等,如果相等则条件为真。 (A == B) 不为真。
!= 检查两个操作数的值是否相等,如果不相等则条件为真。 (A != B) 为真。
> 检查左操作数的值是否大于右操作数的值,如果是则条件为真。 (A > B) 不为真。
< 检查左操作数的值是否小于右操作数的值,如果是则条件为真。 (A < B) 为真。
>= 检查左操作数的值是否大于或等于右操作数的值,如果是则条件为真。 (A >= B) 不为真。
<= 检查左操作数的值是否小于或等于右操作数的值,如果是则条件为真。 (A <= B) 为真。

逻辑运算符

假设变量 A 的值为 1,变量 B 的值为 0,则:

运算符 描述 实例
&& 称为逻辑与运算符。如果两个操作数都 true,则条件为 true。 (A && B) 为 false。
|| 称为逻辑或运算符。如果两个操作数中有任意一个 true,则条件为 true。 (A || B) 为 true。
! 称为逻辑非运算符。用来逆转操作数的逻辑状态,如果条件为 true 则逻辑非运算符将使其为 false。 !(A && B) 为 true。

在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语句中,除非你确实想在检查条件的同时赋值。

位运算符

位运算(&、|、^、~、>>、 | 菜鸟教程

符号 描述 运算规则
& 两个位都为1时,结果才为1
| 两个位都为0时,结果才为0
^ 异或 两个位相同为0,相异为1
~ 取反 0变1,1变0
<< 左移 各二进位全部左移若干位,高位丢弃,低位补0。例如,设 a = 1010 1110a = a << 2a 的二进制位左移2位、右补0,即得 a = 1011 1000。若左移时舍弃的高位不包含1,则每左移一位,相当于该数乘以2。
>> 右移 各二进位全部右移若干位,高位补0或符号位补齐,右边丢弃。操作数每右移一位,相当于该数除以2。

赋值运算符

运算符 描述 实例
= 简单的赋值运算符,把右边操作数的值赋给左边操作数 C = A + B 将把 A + B 的值赋给 C
+= 加且赋值运算符,把右边操作数加上左边操作数的结果赋值给左边操作数 C += A 相当于 C = C + A
-= 减且赋值运算符,把左边操作数减去右边操作数的结果赋值给左边操作数 C -= A 相当于 C = C - A
*= 乘且赋值运算符,把右边操作数乘以左边操作数的结果赋值给左边操作数 C *= A 相当于 C = C * A
/= 除且赋值运算符,把左边操作数除以右边操作数的结果赋值给左边操作数 C /= A 相当于 C = C / A
%= 求模且赋值运算符,求两个操作数的模赋值给左边操作数 C %= A 相当于 C = C % A
<<= 左移且赋值运算符 C <<= 2 等同于 C = C << 2
>>= 右移且赋值运算符 C >>= 2 等同于 C = C >> 2
&= 按位与且赋值运算符 C &= 2 等同于 C = C & 2
^= 按位异或且赋值运算符 C ^= 2 等同于 C = C ^ 2
|= 按位或且赋值运算符 C |= 2 等同于 C = C | 2

组织管理一组变量或函数

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

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

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

类与对象(面向对象的编程)

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

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

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

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

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

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

怎么用类与对象

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

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

封装有以下几个优点:

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

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

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

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

函数重载允许在同一个作用域中定义多个具有相同名称但参数列表不同的函数。参数列表的不同之处可以是参数的数量、类型或顺序。编译器通过参数列表来区分这些重载函数。

成员变量和成员函数的访问权限

成员变量

通常是private

  • 封装性:将成员变量设为private可以隐藏类的内部实现细节,防止外部代码直接修改类的成员变量,从而确保数据的一致性和完整性。
  • 控制访问:通过提供公共的访问接口(如getter和setter函数),可以更好地控制对成员变量的访问和修改。这使得在需要时可以添加额外的逻辑(如验证、日志记录等)。
  • 易于维护:如果成员变量是private的,则可以随时修改类的内部实现而不影响类的外部接口,从而提高代码的可维护性。

成员函数(方法)

通常是public

  • 接口定义:成员函数定义了类的公共接口,使得外部代码可以通过这些函数与类进行交互(class.function())。这些函数通常是public的,以便被外部代码调用。

    private成员函数只能被同一个类的其他成员函数或友元函数访问。这种封装机制可以防止外部代码绕过类的接口直接调用内部实现,确保类的内部实现细节对外部代码是隐藏的。

  • 操作封装:成员函数可以对private成员变量进行操作,从而实现数据的封装和逻辑的分离。

    类的成员函数无论是privateprotected还是public,都可以访问和修改同一个类的private成员变量。

模板类

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

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++中的广泛应用,不仅限于类和结构体。

指针与引用

区别

1
2
3
4
5
6
7
8
9
// 指针
int a = 10;
int* ptr = &a; // 获取a的地址
int value = *ptr; // 间接访问a的值

// 引用
int a = 10;
int& ref = a; // ref是a的引用
int value = ref; // 直接访问a的值
  1. 定义方式:
    • 指针:指针是一个变量,它存储另一个变量的内存地址。
    • 引用:引用是一个变量的别名,它必须在声明时进行初始化,并且不能改变引用的对象。
  2. 初始化:
    • 指针:指针可以在声明后再进行初始化,并且可以指向不同的对象。
    • 引用:引用必须在声明时进行初始化,并且不能改变引用的对象。
  3. 重新绑定:
    • 指针:指针可以重新指向不同的对象。
    • 引用:引用一旦绑定到一个对象,就不能重新绑定到另一个对象。
  4. 空值:
    • 指针:指针可以是空指针,表示它不指向任何对象。
    • 引用:引用不能是空的,它必须引用一个有效的对象。
  5. 操作符:
    • 指针:使用*操作符来间接访问指针指向的对象(在函数内部需要显式地解引用(*)),使用&操作符来获取变量的地址。
    • 引用:使用引用名直接访问引用的对象。
  6. 常见用途:
    • 指针:常用于动态内存分配、数组和数据结构(如链表、树)中。
    • 引用:常用于函数参数和返回值,以避免拷贝大对象,提高性能。
  7. 等等。

指针

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
#include <iostream>

using namespace std;

int main ()
{
int var = 20; // 实际变量的声明
int *ip; // 指针变量的声明

ip = &var; // 在指针变量中存储 var 的地址

cout << "Value of var variable: ";
cout << var << endl;

// 输出在指针变量中存储的地址
cout << "Address stored in ip variable: ";
cout << ip << endl;

// 访问指针中地址的值
cout << "Value of *ip variable: ";
cout << *ip << endl;

return 0;
}

Value of var variable: 20
Address stored in ip variable: 0xbfc601ac
Value of *ip variable: 20

*edge_image_ptr是在操作指针指向的对象,而edge_image_ptr是在操作指针本身。

引用

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
#include <iostream>

using namespace std;

int main ()
{
// 声明简单的变量
int i;
double d;

// 声明引用变量
int& r = i;
double& s = d;

i = 5;
cout << "Value of i : " << i << endl;
cout << "Value of i reference : " << r << endl;

d = 11.7;
cout << "Value of d : " << d << endl;
cout << "Value of d reference : " << s << endl;

return 0;
}

Value of i : 5
Value of i reference : 5
Value of d : 11.7
Value of d reference : 11.7

swap函数

C++ swap(交换)函数 指针/引用/C自带_cswap-CSDN博客

指针写法:

1
2
3
4
5
6
7
8
9
10
11
void myswap(int *a,int *b){//传入的是a和b的地址值
int tmp=*a; //除了声明或初始化指针时,*代表指针的意思,其他时候*在指针变量名左边代表指针指向的内容。这里tmp=a地址指向的内容,即a的值
*a=*b; //指针a指向的地址空间内容变成指针b指向的地址空间内容,即a=b
*b=tmp; //同理b指向的地方=tmp/a,完成交换
}
void main(){
int a=1,b=2;
myswap(&a,&b); //传入地址值
printf("%d %d",a,b); // 输出为2 1
return 0;
}

引用写法:

1
2
3
4
5
6
7
8
9
10
11
void myswap(int &a,int &b){  //传入的为引用,调用该函数的时候,会生成引用a=a,引用b=b,所以函数里操作引用的时候,就是修改了原值。
int tmp=a;
a=b;
b=tmp;
}
void main(){
int a=1,b=2;
myswap(a,b);
printf("%d %d",a,b); // 2 1
return 0;
}

指针的类型

野指针?悬空指针? 一文带你搞懂!

野指针

野指针是指尚未初始化的指针,既不指向合法的内存空间,也没有使用 NULL/nullptr 初始化指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
using namespace std;

int main()
{
int *p; // 野指针
int *q = NULL; // 非野指针
p = new int(5); // p 现在不再是野指针
q = new int(10);
cout<<"*p = "<<*p<<endl; // *p = 5
cout<<"*q = "<<*q<<endl; // *q = 10
free(p);
free(q);
return 0;
}

悬空指针

悬空指针是指指针指向的内存空间已被释放或不再有效。

避免悬空指针的常见做法是将指针置为 nullptr

  • 释放指针资源后,未再次赋值前。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #include <iostream>
    using namespace std;

    int main()
    {
    int *p = new int(5);
    cout<<"*p = "<<*p<<endl; // *p = 5
    cout<<"p 地址:"<<p<<endl; // p 地址:0x55a885ef6eb0
    free(p); // p 在释放后成为悬空指针
    cout<<"p 地址:"<<p<<endl; // p 地址:0x55a885ef6eb0
    cout<<"*p = "<<*p<<endl; // *p = 0。free 后 *p 的值,视不同编译器情况而不同。
    p = NULL; // 非悬空指针
    return 0;
    }
  • 超出了变量的作用范围。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #include <iostream>
    using namespace std;

    int main()
    {
    int *p;
    {
    int tmp = 10;
    p = &tmp;
    }
    //p 在此处成为悬空指针
    return 0;
    }
  • 指向了函数局部变量。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #include <iostream>
    using namespace std;

    int* getVal() {
    int tmp = 10;
    return &tmp;
    }

    int main()
    {
    int *p = getVal(); //悬空指针
    cout<<"*p = "<<*p<<endl;
    return 0;
    }
  • 完。

未释放内存而丢失指针引用会导致内存泄漏

指针的操作

delete

delete 是C++中的运算符,用于释放由new分配的内存。

当你使用 delete 操作符时,首先会调用指针所指向对象的析构函数(如果有的话),然后释放那块内存。因此,指针指向的内存会被释放,但指针变量本身仍然存在,只是它现在指向一个无效地址(悬空指针)。

1
2
3
4
5
6
7
8
#include <iostream>

int main() {
int* ptr = new int(5); // 动态分配内存
delete ptr; // 释放内存,ptr现在是悬空指针
ptr = nullptr; // 避免悬空指针
return 0;
}

free

free() 函数是C标准库中的函数,用于释放由malloc()calloc()realloc()分配的内存。

当你使用 free() 函数时,它只会释放指针指向的内存,不会调用析构函数。因此,指针指向的内存会被释放,但指针变量本身仍然存在,只是它现在指向一个无效地址(悬空指针)。

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

int main() {
int* ptr = (int*)malloc(sizeof(int)); // 动态分配内存
*ptr = 5;
free(ptr); // 释放内存,ptr现在是悬空指针
ptr = nullptr; // 避免悬空指针
return 0;
}

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();
}
}
}
}

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;
}

内存泄漏

参考链接

什么是内存泄漏?

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

内存泄漏的原因

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

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

    在C++中,指针本身是一个变量,它存储的是内存地址。指针的生命周期与普通变量相同,当指针超出其作用域时,指针变量本身会被自动销毁,但它指向的内存并不会被自动释放。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    #include <iostream>

    void exampleFunction() {
    int* ptr = new int(10); // 动态分配内存
    std::cout << "Inside function: " << *ptr << std::endl;
    // ptr变量在函数结束时会被销毁,但指向的内存不会被释放
    // delete ptr; // 手动释放内存

    // std::unique_ptr<int> ptr = std::make_unique<int>(10); // 使用智能指针自动管理内存
    // 不需要手动释放内存,智能指针在作用域结束时会自动释放内存
    }

    int main() {
    exampleFunction();
    // 此时ptr已经被销毁,但指向的内存仍然存在,导致内存泄漏
    return 0;
    }
  • 资源占用:除了内存外,程序可能申请其他系统资源(如文件句柄、数据库连接等),未正确释放这些资源也会导致类似内存泄漏的问题。

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

如何判断内存泄漏?

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

  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 类型,然后进行相加运算。这样可以确保运算结果的正确性和避免潜在的溢出问题。

第三方库

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可能是更好的选择。

工具

静态代码分析

https://www.zhihu.com/

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

可以尝试Understand。

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

Source Insight?没Understand好用。

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

具体可以网上搜一下。

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)

其它

  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 < j && i > 0; i++, j--) {
    swap(s[i],s[j]);
    }
    }

    ,;判断条件要用逻辑运算符。

  32. 在C中,如果你在当前的作用域(例如全局作用域、类作用域或命名空间作用域)定义了一个自定义的reverse函数,并且在代码中调用reverse函数时没有明确指定命名空间,那么程序会优先使用你自定义的reverse函数,而不是标准库中的std::reverse函数。这是因为在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
    #include <iostream>
    #include <algorithm> // 包含 std::reverse

    // 自定义的 reverse 函数
    void reverse(int* arr, int size) {
    std::cout << "Calling custom reverse function." << std::endl;
    for (int i = 0; i < size / 2; ++i) {
    std::swap(arr[i], arr[size - 1 - i]);
    }
    }

    int main() {
    int arr[] = {1, 2, 3, 4, 5};
    int size = sizeof(arr) / sizeof(arr[0]);

    // 调用自定义的 reverse 函数
    reverse(arr, size);

    // 输出反转后的结果
    std::cout << "Reversed array: ";
    for (int i = 0; i < size; ++i) {
    std::cout << arr[i] << " ";
    }
    std::cout << std::endl;

    // 如果你想使用标准库的 std::reverse 函数,需要显式地使用命名空间
    std::reverse(arr, arr + size);

    // 输出再次反转后的结果
    std::cout << "Reversed array using std::reverse: ";
    for (int i = 0; i < size; ++i) {
    std::cout << arr[i] << " ";
    }
    std::cout << std::endl;

    return 0;
    }
  33. C++数据类型:

    • short: 通常为16位,表示带符号的短整数,范围约为 -32,768 到 32,767。
    • int: 通常为32位,表示带符号的整数,范围约为 -2,147,483,648 到 2,147,483,647。
    • long: 通常为32位,表示带符号的长整数,范围约为 -2,147,483,648 到 2,147,483,647。
    • long long: 通常为64位,表示带符号的长长整数,范围约为 -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807。

    这些整数数据类型可以是带符号的或无符号的,区别在于它们能够表示的范围和可用的位数。无符号整数仅可以表示非负数(包括零),因此它们的范围是从0到正的最大值。例如,unsigned intunsigned shortunsigned longunsigned long long 是无符号整数类型。

    C++ 中 int、short、long和long long 分别是几位?有符号无符号有什么区别?

  34. switch语句:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    switch(expression){
    case constant-expression :
    statement(s);
    break; // 可选的
    case constant-expression :
    statement(s);
    break; // 可选的

    // 您可以有任意数量的 case 语句
    default : // 可选的
    statement(s);
    }
    • 只能针对基本数据类型中的整型类型使用switch,这些类型包括intchar等。对于其他类型(string),则必须使用if语句。
    • switch()的参数类型不能为实型。
    • case标签必须是常量表达式,如42或者’4’。
  35. 在C++中,单引号 (') 和双引号 (") 有不同的用途:

    1. 单引号 ('):用于表示单个字符(char 类型)。
    2. 双引号 ("):用于表示字符串字面量(const char*std::string 类型)。
  36. 三元运算符或条件运算符。其语法格式如下:

    1
    2
    condition ? expression_if_true : expression_if_false;
    // max_value = node->val > max_value ? node->val : max_value;
  37. 在C++中,void函数表示不返回任何值,

    1. 简洁代码:如果函数逻辑简单且没有复杂的分支,通常不需要显式地写上return;,代码会显得更加简洁。
    2. 一致性:如果函数有多个分支或条件语句,显式地写上return;可以提高代码的一致性和可读性,明确表示函数的结束。
  38. std::endl 是一个操纵符(manipulator),用于在输出流中插入换行符并刷新缓冲区。如果不加 std::endl,结果不会自动换行并且缓冲区也不会立即刷新。

    如果你不加 std::endl,可以使用换行符 \n 来代替它:

    1
    2
    // std::cout << result << std::endl;
    std::cout << result << "\n";

    这会插入一个换行符,但是不会自动刷新缓冲区。缓冲区什么时候刷新取决于以下几个因素:

    1. 缓冲区满:当缓冲区满时,系统会自动刷新。
    2. 程序正常结束:在程序正常结束时,缓冲区会被刷新。
    3. 显式刷新:可以使用 std::flush 来显式刷新缓冲区。
  39. 等等。


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