告别 C 语言:现代 C++ 引用、RAII 思想与高频 STL

3870 字
19 分钟
告别 C 语言:现代 C++ 引用、RAII 思想与高频 STL

上一章你已经摸清了 C 语言的底层真相:指针、内存、结构体对齐。虽然 C 语言能写一切,但如果你真的用纯 C 来开发一整套视觉识别 + 串口通信系统,你会发现每一行代码都是在手动搬砖——malloc 完了不敢忘 free,数组多大得自己记,换一个新数据源就得重写一堆重复逻辑。
现代 C++ 就是把这些苦力活自动化的武器。这一章讲的是视觉上位机开发中真正天天在用的特性。


一、引用(Reference):干掉大部分裸指针#

1.1 什么是引用?#

引用就是给一个已存在的变量起一个别名。它和指针最大的区别是:引用一旦绑定就不能改,而且不存在”空引用”。

int a = 42;
int &ref = a; // ref 是 a 的别名,它们就是同一个东西
ref = 100;
printf("a = %d\n", a); // 100,改 ref 就是改 a

你可以把引用理解为”指针的语法糖”——底层干的事几乎一样(传地址),但表面上你不需要写 * 和 & 来解引用和取地址了,代码干净很多。

1.2 函数传参:引用 vs 指针#

上一章讲了 C 语言想在函数里修改外部变量必须传指针。在 C++ 中,用引用可以做到同样的事,但写起来和用普通变量一模一样:

// C 风格:传指针
void detect_c(float *out_x, float *out_y) {
*out_x = 1.5f;
*out_y = 2.3f;
}
// C++ 风格:传引用(推荐)
void detect_cpp(float &out_x, float &out_y) {
out_x = 1.5f; // 直接赋值,不需要 * 解引用
out_y = 2.3f;
}
int main() {
float x = 0, y = 0;
detect_c(&x, &y); // C:调用时要取地址
detect_cpp(x, y); // C++:直接传变量名,干净!
return 0;
}

1.3 const 引用:只读保证与传参法则#

如果你不希望函数修改传入的数据,加上 const:

// 结构体或大对象:使用 const 引用避免深拷贝
void process_target(const Target &t) {
// 只能读取 t 的成员,试图修改 t.x = 10 会在编译期直接报错
t.print();
}

传参黄金法则(高频避坑)#

  1. 原生基本类型(int, float, double, bool 等):直接传值!
    它们只有 1~8 字节,直接通过 CPU 寄存器传递极快。如果写成 const float &x,反而强制生成了一个 8 字节的指针地址,CPU 还需要一次额外的内存解引用寻址,纯属负优化;
  2. 复合大对象(结构体、std::string、std::vector 等):必须传 const &!
    避免每次函数调用都拷贝整段数据;
  3. 关于 OpenCV cv::Mat 的特别说明:
    后面第六章我们会详细剖析:cv::Mat 赋值是浅拷贝(只复制约 96 字节的矩阵头,并不拷贝底层的兆级像素数据)。但按值传递依然会触发一次矩阵头拷贝,并对内部的原子引用计数进行一次增减操作(涉及 CPU 内存屏障)。在 60~100 FPS 的高频视觉循环中,依然推荐使用 const cv::Mat &img 传参,消除一切原子操作开销并明确只读语义。

二、类(Class)与 RAII:资源自动管理#

2.1 从结构体到类#

C 语言的 struct 只能装数据。C++ 的 class 可以同时装数据 + 操作这些数据的函数(方法):

class Target {
public:
int color;
float x, y, z;
float confidence;
// 成员函数:计算到相机的距离
float distance() const {
return std::sqrt(x * x + y * y + z * z);
}
// 成员函数:打印目标信息
void print() const {
printf("[目标] 颜色:%d 坐标:(%.2f, %.2f, %.2f) 距离:%.2f米 置信度:%.0f%%\n",
color, x, y, z, distance(), confidence * 100);
}
};
int main() {
Target t1 = {1, 0.5f, 1.2f, 3.0f, 0.95f};
t1.print();
return 0;
}

public: 表示这些成员谁都能访问。还有 private:(只有类自己的成员函数能访问)和 protected:。在初学阶段先全部用 public,后面实际项目中需要保护内部状态时再用 private。

2.2 构造函数与析构函数#

构造函数:对象被创建时自动调用,用来初始化。
析构函数:对象被销毁时自动调用,用来清理资源。

#include <cstdio>
class SerialPort {
public:
int fd; // 文件描述符(Linux 下串口用 fd 打开)
// 构造函数:对象一创建就打开串口
SerialPort(const char *device) {
printf("打开串口: %s\n", device);
// fd = open(device, O_RDWR); // 真实代码
fd = 42; // 模拟
}
// 析构函数:对象销毁时自动关闭串口
~SerialPort() {
printf("关闭串口 fd=%d\n", fd);
// close(fd); // 真实代码
}
// 工业级规范:独占资源禁止拷贝!
// 防止 SerialPort p2 = p1 导致同一个 fd 被析构两次 (Double Close 灾难)
SerialPort(const SerialPort&) = delete;
SerialPort& operator=(const SerialPort&) = delete;
void send(const uint8_t *data, int len) {
printf("发送 %d 字节\n", len);
// write(fd, data, len); // 真实代码
}
};
int main() {
{
SerialPort serial("/dev/ttyUSB0"); // 构造:自动打开
uint8_t packet[] = {0xA5, 0x01, 0x02};
serial.send(packet, 3);
} // 出了这个大括号,serial 对象销毁,析构函数自动调用 → 自动关闭串口!
printf("串口已经被自动关闭了\n");
return 0;
}

2.3 RAII:现代 C++ 最核心的思想#

上面的例子就是 RAII(Resource Acquisition Is Initialization,资源获取即初始化) 的体现:

  • 资源在构造函数中获取(打开文件、分配内存、连接相机);
  • 资源在析构函数中释放(关闭文件、释放内存、断开相机);
  • 只要对象的生命周期结束,资源保证被释放,不需要你手动 close()、free()。

这就是为什么现代 C++ 几乎不需要手动 malloc/free!所有资源(内存、文件、串口、相机连接)都可以绑定到一个对象上,让 C++ 的作用域机制帮你自动管理。
如果你记住一句话就够了:在 C++ 中,永远不要在外面裸写 new/delete 或 malloc/free,把它们藏进对象的构造和析构函数里。


三、STL 容器:C++ 的标准军火库#

STL(Standard Template Library,标准模板库)是 C++ 自带的数据结构和算法集合。用了 STL,你再也不需要手写动态数组、链表、排序算法。

3.1 std::vector:动态数组(用得最多的容器,没有之一)#

C 语言的数组大小必须编译时确定,或者手动 malloc 扩容——极其痛苦。std::vector 帮你全部搞定:

#include <vector>
#include <cstdio>
int main() {
// 创建一个空的 float 动态数组
std::vector<float> distances;
// 往末尾追加元素(自动扩容,不用管内存)
distances.push_back(1.5f);
distances.push_back(2.3f);
distances.push_back(0.8f);
// 获取大小
printf("共 %zu 个距离值\n", distances.size()); // 3
// 用下标访问(和普通数组一样)
printf("第一个: %.1f\n", distances[0]); // 1.5
// 遍历
for (int i = 0; i < (int)distances.size(); i++) {
printf("distances[%d] = %.1f\n", i, distances[i]);
}
// 不需要 free!vector 出了作用域自动释放内存(RAII!)
return 0;
}

在视觉代码中的典型用法#

// 存储识别到的所有目标
struct Target {
int color;
float x, y, z;
};
std::vector<Target> targets;
// 每识别到一个目标就 push_back 进去
targets.push_back({1, 0.5f, 1.2f, 3.0f});
targets.push_back({2, 1.0f, 0.8f, 2.5f});
// 按距离排序、找最近的那个、删除不合格的……全部有现成方法

vector 可以替代你 90% 的 C 语言动态数组需求。从现在开始,忘掉 malloc 数组,全部用 vector。

3.2 std::string:告别 char[] 的噩梦#

#include <string>
#include <iostream>
int main() {
std::string name = "red_block";
// 拼接
std::string msg = "检测到: " + name;
std::cout << msg << std::endl; // "检测到: red_block"
// 长度
std::cout << "长度: " << name.length() << std::endl; // 9
// 比较(不用 strcmp 了!)
if (name == "red_block") {
std::cout << "匹配!" << std::endl;
}
// 查找子串
if (name.find("block") != std::string::npos) {
std::cout << "包含 block" << std::endl;
}
return 0;
}

再也不用担心 char[] 缓冲区溢出了。std::string 自动管理内存,拼接、比较、查找全是现成方法。

3.3 std::pair 与 std::tuple:临时打包多个返回值#

#include <utility> // pair
#include <tuple> // tuple
// 返回目标的像素坐标 (u, v)
std::pair<int, int> get_pixel_center() {
return {320, 240}; // 直接用花括号构造
}
// 返回三维坐标
std::tuple<float, float, float> get_3d_position() {
return {0.5f, 1.2f, 3.0f};
}
int main() {
auto [u, v] = get_pixel_center(); // C++17 结构化绑定
auto [x, y, z] = get_3d_position();
printf("像素: (%d, %d), 空间: (%.1f, %.1f, %.1f)\n", u, v, x, y, z);
return 0;
}

3.4 std::queue:先进先出队列#

后面学工业相机多线程取流时会用到——相机线程往队列里塞图像帧,算法线程从队列里取最新帧:

#include <queue>
std::queue<int> frame_queue;
// 入队(相机线程塞帧)
frame_queue.push(1);
frame_queue.push(2);
frame_queue.push(3);
// 出队(算法线程取帧)
while (!frame_queue.empty()) {
int frame_id = frame_queue.front(); // 看队头
frame_queue.pop(); // 弹出队头
printf("处理帧 %d\n", frame_id);
}

并发安全警示:
std::queue 本身不是线程安全的!如果在两个不同的线程中同时调用 push 和 pop,会发生数据竞争导致程序段错误崩溃。后续在第九章讲工业相机多线程取流时,我们会用互斥锁(std::mutex)将它封装为线程安全的单缓冲队列。

3.5 std::map:键值对映射#

#include <map>
#include <string>
// 存储颜色编号到名称的映射
std::map<int, std::string> color_names;
color_names[1] = "红色";
color_names[2] = "蓝色";
color_names[3] = "黄色";
printf("编号1对应: %s\n", color_names[1].c_str());

四、现代 C++ 语法糖:写更少的代码做更多的事#

4.1 auto:让编译器帮你推导类型#

auto a = 42; // int
auto b = 3.14f; // float
auto s = std::string("hello"); // std::string
// 最常用的场景:迭代器类型太长了,用 auto 省心
std::vector<Target> targets;
// 不用 auto:
// std::vector<Target>::iterator it = targets.begin();
// 用 auto:
auto it = targets.begin(); // 编译器自动推导

auto 不是弱类型!编译期就已经确定了具体类型,只是你不用手写罢了。

4.2 范围 for 循环(Range-based for)#

C 风格遍历数组要写下标和 size(),范围 for 一行搞定:

std::vector<float> distances = {1.5f, 2.3f, 0.8f};
// C 风格
for (int i = 0; i < (int)distances.size(); i++) {
printf("%.1f\n", distances[i]);
}
// 现代 C++ 风格(推荐)
for (float d : distances) {
printf("%.1f\n", d);
}
// 如果要修改元素,加引用
for (float &d : distances) {
d *= 100; // 将单位从米转为厘米
}
// 只读遍历大对象,用 const 引用避免拷贝
std::vector<Target> targets;
for (const Target &t : targets) {
printf("(%.2f, %.2f, %.2f)\n", t.x, t.y, t.z);
}

4.3 Lambda 表达式:匿名小函数#

Lambda 最常见的用途是给 std::sort 等算法传自定义比较规则:

#include <algorithm>
#include <vector>
struct Target {
int color;
float x, y, z;
float distance() const { return std::sqrt(x*x + y*y + z*z); }
};
int main() {
std::vector<Target> targets = {
{1, 0.5f, 1.2f, 3.0f},
{2, 1.0f, 0.8f, 1.5f},
{1, 0.2f, 0.3f, 0.5f},
};
// 按距离从近到远排序
std::sort(targets.begin(), targets.end(),
[](const Target &a, const Target &b) {
return a.distance() < b.distance(); // 返回 true 则 a 排在 b 前面
}
);
// 现在 targets[0] 就是最近的那个目标
printf("最近目标距离: %.2f 米\n", targets[0].distance());
return 0;
}

Lambda 的基本语法:[捕获列表](参数) { 函数体 }

  • []:不捕获外部变量。
  • [&]:按引用捕获所有外部变量。
  • [=]:按值捕获所有外部变量。
  • [&dist_threshold]:只按引用捕获特定变量。

五、智能指针:彻底告别 new/delete#

5.1 为什么需要智能指针?#

上一章说了 malloc 忘 free 会内存泄漏。C++ 的 new/delete 同理:

Target *t = new Target{1, 0.5f, 1.2f, 3.0f, 0.9f};
// 用完了...
delete t; // 忘了这行就泄漏!

智能指针的思路和 RAII 一脉相承——用一个对象来”持有”堆上的内存,对象销毁时自动释放。

5.2 std::unique_ptr:独占所有权(最常用)#

#include <memory>
int main() {
// 创建一个 unique_ptr,独占管理一个 Target
auto target = std::make_unique<Target>();
target->color = 1;
target->x = 0.5f;
target->y = 1.2f;
target->z = 3.0f;
target->print(); // 正常使用,和裸指针一样用 ->
// 不需要 delete!
// target 出了作用域时,unique_ptr 的析构函数自动 delete 内部指针
return 0;
}

unique_ptr 是”独占”的,不能被复制(拷贝),但可以被”移动”(转让所有权)。这保证了同一块内存在任意时刻只有一个管理者,杜绝了双重释放(double free)的问题。

5.3 std::shared_ptr:共享所有权#

有时多个地方需要同时持有同一块数据(比如多个模块都要读取同一帧图像)。shared_ptr 内部维护一个引用计数,最后一个 shared_ptr 销毁时才真正释放内存:

#include <memory>
void process_frame(const std::shared_ptr<FrameData>& frame) {
// 传 const 引用:避免原子引用计数的无谓增减与内存屏障开销
// 处理图像...
}
int main() {
auto frame = std::make_shared<FrameData>(); // 引用计数 = 1
process_frame(frame);
// frame 出作用域时引用计数归 0 → 自动释放
return 0;
}

在初学阶段,优先用 unique_ptr。只有在”确实需要多个地方共享同一份数据”时才考虑 shared_ptr。
另外在性能敏感的热点代码(如 60 FPS 视觉循环)中,如果函数只是只读读取数据,优先传 const std::shared_ptr<T>& 或直接传解引用后的对象引用 const T&,避免频繁触发原子引用计数的增减。


六、#include 与命名空间#

6.1 C++ 标准头文件#

你可能注意到了 C++ 的头文件不带 .h:

#include <cstdio> // C++ 版的 stdio.h
#include <cstdlib> // C++ 版的 stdlib.h
#include <cmath> // C++ 版的 math.h
#include <cstdint> // C++ 版的 stdint.h
#include <vector> // C++ 独有
#include <string> // C++ 独有
#include <memory> // C++ 独有(智能指针)
#include <algorithm> // C++ 独有(sort 等算法)

C 语言的头文件(如 <stdio.h>)在 C++ 里也能用,但推荐用 <cstdio> 这种 C++ 包装版本——它把所有函数放进了 std 命名空间里。

6.2 命名空间(Namespace)#

// 标准库的所有东西都在 std 命名空间里
std::vector<int> v;
std::string s;
std::cout << "hello" << std::endl;
// 觉得写 std:: 太烦?可以在文件顶部声明(不推荐在头文件中这样做)
using namespace std;
vector<int> v;
string s;

在 .cpp 源文件里写 using namespace std; 无所谓。但绝对不要在 .h 头文件里写——它会污染所有包含这个头文件的文件的命名空间,容易引发名字冲突。


七、C 与 C++ 的编译差异#

方面CC++
源文件后缀.c.cpp
编译器gccg++
头文件<stdio.h><cstdio>
布尔类型_Bool(需 <stdbool.h>)bool(内置)
字符串char[] + strlen/strcmpstd::string
动态数组malloc/realloc/freestd::vector
内存管理手动 freeRAII + 智能指针

编译命令的变化:

Terminal window
# C 语言
gcc main.c target.c -o my_robot -lm
# C++(只需换成 g++,加上 C++ 标准版本)
g++ -std=c++17 main.cpp target.cpp -o my_robot

从现在开始,我们的所有代码文件都使用 .cpp 后缀,编译器使用 g++。下一章会用 CMake 来管理编译,彻底告别手动敲 g++ 长命令。


🎯 本章通关小作业#

作业 1:用类封装一个目标管理器#

创建一个 TargetManager 类:

  • 内部用 std::vector<Target> 存储所有识别到的目标;
  • 提供 void add(Target t) 方法往里添加目标;
  • 提供 Target get_nearest() 方法返回距离相机最近的那个(用 std::sort + Lambda);
  • 提供 void print_all() 方法打印所有目标的信息;
  • 在 main() 中创建对象,添加 3~5 个假数据目标,调用 get_nearest() 并打印结果。

防御性编程进阶思考:
在真实比赛现场,某一帧画面里一个道具都没检测到(targets.empty() == true)是家常便饭。如果直接访问 targets[0] 程序会瞬间越界崩溃!
思考:当容器为空时,get_nearest() 应该怎么处理?(提示:可以返回一个特殊标志的无效 Target,或者在 C++17 中使用 std::optional<Target>)。

作业 2:体验 RAII 与智能指针#

  • 创建一个 CameraSimulator 类,构造函数里打印 "相机已连接",析构函数里打印 "相机已断开";
  • 在 main() 中用 std::make_unique<CameraSimulator>() 创建对象;
  • 观察程序运行结束后析构函数是否自动调用——你不需要写任何手动释放代码。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

告别 C 语言:现代 C++ 引用、RAII 思想与高频 STL
https://firefly-7a0.pages.dev/posts/rc_basic/04_modern_cpp_primer/
作者
lumivers
发布于
2026-10-08
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
lumivers
Hello, I'm lumivers.
公告
欢迎来到我的博客!
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
182
分类
14
标签
576
总字数
384,899
运行时长
0 天
最后活动
0 天前

目录