3. C++基础回顾(上):指针与引用、内存管理、智能指针、STL容器

好,咱们正式开始啃C++这块硬骨头。做边缘AI推理,C++是绕不开的。你想想看,Python虽然爽,但到了嵌入式设备上,那点性能根本不够看。我这些年踩过的坑,十有八九都跟内存和指针有关。今天咱们就把这几个最基础、也最容易出问题的地方捋清楚。

3.1 指针与引用:看似相似,实则天差地别

指针和引用,说白了都是“间接访问”的工具。但它们的性格完全不同。

指针,像个自由散漫的浪子。它可以不初始化(野指针),可以指向别处,可以为空。我早期做嵌入式项目时,就吃过野指针的大亏——程序跑着跑着就崩了,查了两天才发现是指针没初始化就用了。

引用,像个守规矩的公务员。它必须在定义时初始化,而且一旦绑定就不能再改。引用本质上是个“别名”,操作引用就是操作原变量。

核心区别速览:

特性指针引用
是否可为空可以(nullptr)不可以
是否可重新绑定可以不可以
是否需要初始化不强制(但强烈建议)必须
内存占用通常8字节(64位)不占额外空间(编译器优化)
// 指针示例
int a = 10;
int* p = &a;   // p指向a
*p = 20;       // a变成20
p = nullptr;   // p可以指向空

// 引用示例
int b = 30;
int& ref = b;  // ref是b的别名
ref = 40;      // b变成40
// ref = ...   // 不能改变ref的绑定对象

我的建议:函数参数传递时,能用引用就别用指针。引用更安全,语义也更清晰。除非你需要“可空”语义,才考虑指针。

3.2 内存管理:new/delete 与 RAII 思想

C++的内存管理,说白了就是“谁申请,谁释放”。但现实往往很残酷——你申请了,忘了释放,内存泄漏就来了。

newdelete 是C++的标配。但要注意,new[] 必须配 delete[]new 必须配 delete。混用会导致未定义行为,程序可能直接崩掉。

// 单个对象
int* p = new int(42);
delete p;

// 数组
int* arr = new int[100];
delete[] arr;

嗯,这里要注意:new 和 delete 是操作符,不是函数。它们底层会调用 operator new 和 operator delete,但咱们一般不用关心这个。

我曾经踩过的坑:在一个循环里不断 new 对象,但忘记 delete。跑了一晚上,内存涨到几个G,程序直接 OOM 被系统杀了。从那以后,我养成了“new 完立刻写 delete”的习惯。

RAII(资源获取即初始化)是C++里最优雅的设计之一。说白了就是:把资源的生命周期绑定到对象的生命周期上。对象构造时获取资源,析构时释放资源。这样你就不用操心手动释放了。

3.3 智能指针:现代C++的救星

手动管理内存太累了,而且容易出错。C++11 引入了智能指针,说白了就是 RAII 思想在指针上的应用。

3.3.1 std::unique_ptr:独占所有权

unique_ptr 是“独生子”。它独占所指向的对象,不允许拷贝,只能移动。你想想看,这就像一把钥匙只能一个人拿着。

#include <memory>

std::unique_ptr<int> uptr = std::make_unique<int>(42);
// std::unique_ptr<int> uptr2 = uptr;  // 编译错误!不能拷贝
std::unique_ptr<int> uptr3 = std::move(uptr);  // 可以移动,uptr变为空

我个人习惯在绝大多数场景下用 unique_ptr。它零开销,性能跟裸指针一样,但安全得多。

3.3.2 std::shared_ptr:共享所有权

shared_ptr 是“共享的”。它使用引用计数来管理生命周期。当最后一个 shared_ptr 被销毁时,对象才被释放。

std::shared_ptr<int> sptr1 = std::make_shared<int>(100);
{
    std::shared_ptr<int> sptr2 = sptr1;  // 引用计数+1
    // 此时引用计数为2
}
// sptr2 销毁,引用计数降为1
// sptr1 销毁时,引用计数为0,对象被释放

注意:shared_ptr 有性能开销(引用计数的原子操作),而且容易造成循环引用。我建议只在“真正需要共享所有权”时才用它。

3.3.3 std::weak_ptr:打破循环引用

循环引用是 shared_ptr 的噩梦。比如 A 持有 B 的 shared_ptr,B 也持有 A 的 shared_ptr,那它们俩就永远释放不了。

weak_ptr 是个“旁观者”。它不增加引用计数,但可以检查对象是否还活着,并临时提升为 shared_ptr 来使用。

std::shared_ptr<int> sp = std::make_shared<int>(200);
std::weak_ptr<int> wp = sp;

if (auto locked = wp.lock()) {  // 提升为shared_ptr
    std::cout << *locked << std::endl;
} else {
    std::cout << "对象已被释放" << std::endl;
}

3.4 STL容器:vector 与 map 的实战用法

STL容器是C++的宝库。咱们做边缘AI推理,最常用的就是 vectormap

3.4.1 std::vector:动态数组

vector 就是个能自动扩容的数组。它底层是连续内存,所以随机访问很快(O(1)),但中间插入删除慢(O(n))。

#include <vector>

std::vector<int> vec;
vec.push_back(10);
vec.push_back(20);
vec.push_back(30);

// 遍历
for (size_t i = 0; i < vec.size(); ++i) {
    std::cout << vec[i] << " ";
}

// 范围for
for (const auto& v : vec) {
    std::cout << v << " ";
}

// 预分配内存,避免频繁扩容
vec.reserve(1000);

我的经验:如果你知道大概要存多少数据,记得用 reserve() 预分配内存。否则 vector 会反复扩容、拷贝,性能损失很大。我在处理图像数据时,经常先 reserve 好大小。

3.4.2 std::map:有序键值对

map 是基于红黑树实现的有序字典。它的插入、删除、查找都是 O(log n)。如果你不需要有序,可以用 unordered_map(哈希表,平均 O(1))。

#include <map>

std::map<std::string, int> scores;
scores["Alice"] = 95;
scores["Bob"] = 87;
scores["Charlie"] = 92;

// 查找
auto it = scores.find("Bob");
if (it != scores.end()) {
    std::cout << it->first << ": " << it->second << std::endl;
}

// 遍历(按键排序)
for (const auto& [name, score] : scores) {
    std::cout << name << ": " << score << std::endl;
}

注意:operator[] 访问 map 时,如果键不存在,它会自动插入一个默认值。这有时候会带来意想不到的副作用。我建议用 find()at() 来避免这个问题。

3.5 小结:这些基础决定了你的代码质量

指针和引用是C++的根基,内存管理是C++的命门,智能指针是C++的救赎,STL容器是C++的武器库。这四个东西搞不明白,写出来的代码要么崩,要么慢,要么又崩又慢。

我个人觉得,做边缘AI推理,代码的健壮性比性能更重要。因为设备一旦部署出去,你没法像在服务器上那样随时打补丁。所以,多用智能指针,少用裸指针;多用STL容器,少自己造轮子。这些习惯能帮你省下无数调试时间。

下一节咱们继续聊C++的面向对象和模板,这些都是搭建推理框架的必备技能。到时候见。