目录
  1. 1. 一、RAII:资源获取即初始化
    1. 1.1. 1.1 智能指针(Smart Pointers)
    2. 1.2. 1.2 Android AOSP 的 sp/wp
  2. 2. 二、移动语义与右值引用
  3. 3. 三、模板元编程
    1. 3.1. 3.1 SFINAE(替换失败不是错误)
    2. 3.2. 3.2 Type Traits
  4. 4. 四、Lambda 表达式
  5. 5. 五、STL 容器选择指南
  6. 6. 六、线程安全与互斥锁
  7. 7. 七、const 正确性
  8. 8. 八、面试常问题目
【C/C++理论实战技术】BAT最常用的C++技术

一、RAII:资源获取即初始化

RAII(Resource Acquisition Is Initialization)是 C++ 最重要的惯用法之一。核心思想是:将资源的生命周期与对象的生命周期绑定——构造函数获取资源,析构函数释放资源。这确保了即使在异常路径下,资源也能被正确释放。

Android 的 AOSP 代码中大量使用 RAII。以 Binder 驱动的锁管理为例:

// AOSP: frameworks/native/libs/binder/IPCThreadState.cpp
// Scoped 锁的 RAII 包装
class Mutex {
public:
int lock();
void unlock();
};

class AutoMutex {
public:
AutoMutex(Mutex& m) : mLock(m) { mLock.lock(); }
~AutoMutex() { mLock.unlock(); }
private:
Mutex& mLock;
};

void IPCThreadState::waitForResponse(Parcel *reply, status_t *acquireResult) {
AutoMutex _l(mProcess->mThreadCountLock); // 构造时加锁
// ... 临界区代码 ...
// 即使中间抛异常或 return,析构时自动解锁
}

源码路径:frameworks/native/libs/binder/include/binder/Mutex.h

1.1 智能指针(Smart Pointers)

C++11 标准库提供了三种智能指针,Android AOSP 还有自己的实现(sp/wp 即 StrongPointer/WeakPointer,基于轻量级引用计数)。

shared_ptr:共享所有权,引用计数为 0 时释放资源。

#include <memory>

class Connection {
public:
Connection(const std::string& addr) : m_addr(addr) {
// 建立连接
}
~Connection() {
// 关闭连接——shared_ptr 保证一定调用
}
private:
std::string m_addr;
};

// shared_ptr 的使用
void processRequest() {
auto conn = std::make_shared<Connection>("192.168.1.1:8080");
// 即使下面的函数抛异常,conn 的析构也会被调用
doSomething(conn);
}

unique_ptr:独占所有权,不可拷贝,可移动。零开销(与裸指针性能相同)。

// unique_ptr 适合作为工厂函数的返回值
std::unique_ptr<AudioTrack> createAudioTrack(int sampleRate) {
auto track = std::make_unique<AudioTrack>(sampleRate);
if (!track->init()) {
return nullptr; // 自动释放
}
return track; // 移动语义,所有权转移给调用者
}

weak_ptr:弱引用,不增加引用计数,用于打破 shared_ptr 的循环引用。

class Node {
std::string name;
std::vector<std::shared_ptr<Node>> children;
std::weak_ptr<Node> parent; // 关键:parent 用 weak_ptr,避免循环引用

public:
Node(const std::string& n) : name(n) {}
void setParent(std::shared_ptr<Node> p) { parent = p; }
};

1.2 Android AOSP 的 sp/wp

Android 的 Binder IPC 体系使用 sp<T>(强指针)和 wp<T>(弱指针)管理引用计数:

// AOSP: system/core/libutils/include/utils/RefBase.h
class RefBase {
public:
void incStrong(const void* id) const;
void decStrong(const void* id) const;
// ...
};

template<typename T>
class sp {
T* m_ptr;
public:
sp(T* other) : m_ptr(other) {
if (m_ptr) m_ptr->incStrong(this);
}
~sp() {
if (m_ptr) m_ptr->decStrong(this);
}
// ...
};

IBinderBnInterfaceBpInterface 等 Binder 核心类都继承自 RefBase,通过 sp 管理生命周期。

二、移动语义与右值引用

C++11 的移动语义是一个关键优化:对于临时对象,可以将资源”移动”而不是”拷贝”,避免不必要的内存分配。

class Buffer {
char* m_data;
size_t m_size;

public:
// 移动构造函数——"窃取"资源
Buffer(Buffer&& other) noexcept
: m_data(other.m_data), m_size(other.m_size) {
other.m_data = nullptr; // 将原对象置于有效但未指定状态
other.m_size = 0;
}

// 移动赋值运算符
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] m_data; // 释放当前资源
m_data = other.m_data; // 窃取资源
m_size = other.m_size;
other.m_data = nullptr; // 使原对象安全析构
other.m_size = 0;
}
return *this;
}

// 禁用拷贝(或实现拷贝)
Buffer(const Buffer&) = delete;
Buffer& operator=(const Buffer&) = delete;
};

在 Android NDK 开发中,大 Buffer(如相机帧数据、音频数据)通过移动语义传递可以避免不必要的数据拷贝,显著提升性能。

// std::move 将左值转为右值引用,触发移动语义
std::vector<uint8_t> frame1 = captureFrame();
std::vector<uint8_t> frame2 = std::move(frame1); // frame1 的数据被移动到 frame2
// 之后 frame1 为空,但处于合法状态

std::forward 用于完美转发,保持参数的左值/右值属性:

template<typename T>
void wrapper(T&& arg) {
// std::forward<T> 保持 arg 的引用类型
// 如果传入左值 → 左值引用转发;传入右值 → 右值引用转发
target(std::forward<T>(arg));
}

三、模板元编程

C++ 模板不仅是泛型编程的工具,还是一种编译期计算语言。Android 的 HAL(硬件抽象层)和 ART 运行时大量使用模板提升性能和复用。

3.1 SFINAE(替换失败不是错误)

SFINAE 规则是模板元编程的基础:当模板参数替换失败时,编译器不会报错,而是从重载集中排除该模板。

// 编译期判断类型是否有 foo() 方法
template<typename T>
class has_foo {
private:
typedef char yes[1];
typedef char no[2];

template<typename U, U> struct type_check;

template<typename C>
static yes& test(type_check<void (C::*)(), &C::foo>*);

template<typename>
static no& test(...);

public:
static constexpr bool value = (sizeof(test<T>(nullptr)) == sizeof(yes));
};

C++17 引入 if constexpr 后,条件编译变得更简洁:

template<typename T>
void serialize(std::vector<uint8_t>& buffer, const T& value) {
if constexpr (std::is_integral_v<T>) {
// 整数类型的序列化路径
buffer.insert(buffer.end(),
reinterpret_cast<const uint8_t*>(&value),
reinterpret_cast<const uint8_t*>(&value) + sizeof(T));
} else if constexpr (std::is_same_v<T, std::string>) {
// 字符串的序列化路径(先写长度,再写内容)
uint32_t len = value.size();
serialize(buffer, len);
buffer.insert(buffer.end(), value.begin(), value.end());
} else {
static_assert(sizeof(T) == 0, "Unsupported type for serialization");
}
}

3.2 Type Traits

标准库的 type_traits 头文件提供了大量编译期类型判断和转换的工具:

#include <type_traits>

static_assert(std::is_same_v<int, int32_t>); // 类型相同判断
static_assert(std::is_base_of_v<Base, Derived>); // 继承关系判断
static_assert(std::is_constructible_v<MyClass, int, double>); // 可构造判断

using Decayed = std::decay_t<const int&>; // → int(去除引用和 cv 限定符)
using Underlying = std::underlying_type_t<EnumClass>; // 枚举底层类型

在 Android NDK 中的实际应用——类型安全的资源管理:

// 确保只有继承自 RefBase 的类型才能被 sp 包装
template<typename T>
class sp {
static_assert(std::is_base_of_v<RefBase, T>,
"sp<T>: T must be a subclass of RefBase");
// ...
};

四、Lambda 表达式

C++ lambda 是 Android NDK 开发中最常用的语法糖之一,广泛应用于回调、线程任务、STL 算法等场景。

// 捕获列表语法
auto byValue = [value]() { return value * 2; }; // 拷贝捕获
auto byRef = [&value]() { value *= 2; }; // 引用捕获
auto all = [=]() { return x + y + z; }; // 拷贝捕获所有
auto allRef = [&]() { x = y = z = 0; }; // 引用捕获所有
auto mixed = [=, &result]() { result = a + b; }; // result 引用捕获,其余拷贝

// 线程中使用 lambda
std::thread task([buffer = std::move(buffer)]() {
processBuffer(buffer); // 移动捕获,转移所有权给线程
});
task.join();

// STL 算法中使用 lambda
std::vector<int> values = {5, 3, 1, 4, 2};
std::sort(values.begin(), values.end(),
[](int a, int b) { return a > b; }); // 降序排列

auto it = std::find_if(values.begin(), values.end(),
[threshold = 3](int x) { return x > threshold; });

注意:lambda 捕获引用时,必须确保 lambda 执行时引用仍然有效:

// BAD:lambda 捕获了局部变量的引用,但函数返回后 lambda 才执行
std::function<void()> createBadCallback() {
int local = 42;
return [&local]() { std::cout << local; }; // local 引用悬挂!
}

// GOOD:拷贝捕获值
std::function<void()> createGoodCallback() {
int local = 42;
return [local]() { std::cout << local; }; // 安全
}

五、STL 容器选择指南

Android NDK 开发中的容器选择直接影响性能。以下是关键容器的特性对比和选择建议:

容器 底层 随机访问 插入/删除 内存 适用场景
vector 动态数组 O(1) O(n) 连续 默认首选,尾部操作
deque 分段数组 O(1) O(1) 两端 分段 双端队列
list 双向链表 O(n) O(1) 分散,每元素额外 2 指针 大量中间插入/删除
set/map 红黑树 O(log n) O(log n) 分散 有序集合/映射
unordered_set/map 哈希表 O(1) 均摊 O(1) 均摊 分散,有桶开销 快速查找,不关心顺序
// vector 是大多数场景的最佳默认选择
// 连续内存 -> 缓存友好,比 list 的指针跳转快得多
std::vector<Pixel> scanline;
scanline.reserve(1920); // 预分配避免反复扩容

// unordered_map 适合"通过 key 快速查找 value"
std::unordered_map<std::string, std::unique_ptr<Plugin>> pluginRegistry;
auto it = pluginRegistry.find("video_decoder");

// map 适合需要有序遍历的场景
std::map<uint32_t, std::string> sortedLogs; // 按 ID 排序

六、线程安全与互斥锁

#include <mutex>
#include <shared_mutex>

class ThreadSafeCache {
mutable std::shared_mutex m_mutex; // shared_mutex 支持读写锁
std::unordered_map<int, Data> m_cache;

public:
// 写操作——独占锁
void update(int key, Data value) {
std::unique_lock<std::shared_mutex> lock(m_mutex);
m_cache[key] = std::move(value);
}

// 读操作——共享锁(多个读操作可并发)
std::optional<Data> get(int key) const {
std::shared_lock<std::shared_mutex> lock(m_mutex);
auto it = m_cache.find(key);
if (it != m_cache.end()) {
return it->second;
}
return std::nullopt;
}
};

// scoped_lock 同时锁定多个互斥锁(避免死锁)
std::mutex mtx1, mtx2;
void safeTransfer() {
std::scoped_lock lock(mtx1, mtx2); // C++17,RAII 且防死锁
// 对两个资源进行原子操作
}

// call_once 确保只初始化一次(线程安全的单例)
class Singleton {
static std::unique_ptr<Singleton> s_instance;
static std::once_flag s_flag;

public:
static Singleton& getInstance() {
std::call_once(s_flag, []() {
s_instance.reset(new Singleton());
});
return *s_instance;
}
};

源码参考:AOSP 的 system/core/libutils/include/utils/Mutex.h 中,Android 使用 pthread_mutex_t 而非 std::mutex(因为 AOSP 代码长期基于 C++11 之前的标准)。

七、const 正确性

const 是 C++ 类型系统的核心部分,不仅是”不可变”的承诺,也帮助编译器优化和防止 bug:

class AudioBuffer {
int16_t* m_data;
size_t m_size;

public:
// const 成员函数:承诺不修改对象状态
size_t size() const { return m_size; }

// 返回 const 指针:承诺不通过返回值修改数据
const int16_t* data() const { return m_data; }

// 非 const 版本:允许修改
int16_t* data() { return m_data; }

// const 引用参数:承诺不修改参数
void append(const AudioBuffer& other);
};

// const 在函数签名中区分重载
// find() 的 const 版本在 const 对象上调用时对成员加 shared_lock
// find() 的非 const 版本对成员加 unique_lock

八、面试常问题目

Q1: shared_ptr 和 unique_ptr 的区别?什么场景用哪个?

unique_ptr 独占所有权,不能拷贝(只能移动),编译后与裸指针性能相同(零开销)。适合工厂函数返回、容器中管理对象、PIMPL 惯用法。shared_ptr 共享所有权,有引用计数开销(原子操作),适合多个所有者需要共享同一对象的场景。原则:优先用 unique_ptr,只有确实需要共享所有权时才用 shared_ptr。

Q2: 移动语义解决了什么问题?std::move 做了什么?

移动语义解决了不必要的深拷贝开销——对于包含动态分配资源的对象(如 std::vector、std::string),”移动”只需要复制指针(O(1)),而”拷贝”需要复制整个数据(O(n))。std::move 是一个无条件将左值转换为右值引用的类型转换(static_cast),它本身不移动任何东西,只是让编译器选择移动构造函数/移动赋值运算符而不是拷贝版本。

Q3: std::mutex 和 std::shared_mutex 的区别?

std::mutex 是排他锁(互斥锁),同一时刻只有一个线程能获取锁。std::shared_mutex 支持两种锁定模式:shared_lock(读锁,允许并发读)和 unique_lock(写锁,独占)。在读多写少的场景(如缓存访问),shared_mutex 可显著提高并发性能。Android 中 android:hardware:details:utils 命名空间下有类似实现。

Q4: 为什么在 Android NDK 中 vector 通常优于 list?

虽然 list 的理论插入/删除复杂度是 O(1)(vs vector 的 O(n)),但现代 CPU 的缓存架构使得连续内存的访问远快于指针跳转。vector 在连续内存中存储元素,CPU 预取(prefetch)可以高效工作;list 的每个节点分散在堆上,每次跳转都可能造成 cache miss。除非元素非常大(如 > 1KB)且频繁在中间插入/删除,否则 vector 通常更快。


参考源码路径:

  • AOSP RefBase:system/core/libutils/include/utils/RefBase.h
  • AOSP StrongPointer:system/core/libutils/include/utils/StrongPointer.h
  • AOSP Mutex:system/core/libutils/include/utils/Mutex.h
  • C++ Standard Library:https://en.cppreference.com/w/
  • Android NDK 文档:https://developer.android.com/ndk/guides/cpp-support
打赏
  • 微信
  • 支付宝

评论