C/C++ 在音视频中的应用要点 —— 内存 / 指针 / RAII / 字节序

音视频底层 90% 是 C/C++(FFmpeg、x264、libvlc、WebRTC)。Java/Kotlin 工程师转 NDK 必须懂的 C/C++ 实战要点。这一篇讲那些在音视频开发中真正会用到的 C/C++ 知识。


0. 为啥音视频要懂 C/C++

FFmpeg 是 C
x264 / x265 是 C++
libvlc 核心是 C
WebRTC 是 C++
ijkplayer 核心是 C

Android NDK / iOS native 开发都绕不开。这一篇按"频率"列出关键概念。


1. 指针与内存管理

1.1 指针基础

int x = 10;
int* p = &x;     // p 是 x 的地址
*p = 20;          // 通过指针修改 x
printf("%d\n", x);  // 20

1.2 音视频中的常见指针

// AVFrame 拿 YUV plane
AVFrame* frame = av_frame_alloc();
uint8_t* y_plane = frame->data[0];   // Y plane 起始地址
uint8_t* u_plane = frame->data[1];
uint8_t* v_plane = frame->data[2];

int y_stride = frame->linesize[0];   // Y plane 每行字节数

// 读第 i 行第 j 列的 Y 值
uint8_t y_value = y_plane[i * y_stride + j];

1.3 内存分配

// C 风格
uint8_t* buffer = (uint8_t*)malloc(size);
// ... 用 ...
free(buffer);

// FFmpeg 推荐
uint8_t* buffer = av_malloc(size);
av_free(buffer);

// C++ 风格
auto buffer = new uint8_t[size];
delete[] buffer;

// C++ 现代风格 (推荐)
std::unique_ptr<uint8_t[]> buffer(new uint8_t[size]);
// 自动释放, 不用 delete

1.4 内存泄漏 —— 头号大坑

void process_frame() {
    AVFrame* f = av_frame_alloc();   // 分配
    // ... 处理 ...
    if (error_condition) return;     // ⛔ 早退忘记 free
    av_frame_free(&f);
}

✅ 用 RAII(C++):

class FrameRAII {
    AVFrame* frame_;
public:
    FrameRAII() : frame_(av_frame_alloc()) {}
    ~FrameRAII() { av_frame_free(&frame_); }
    AVFrame* get() { return frame_; }
};

void process_frame() {
    FrameRAII f;
    // ... 用 f.get() ...
    // 早退也会自动 free
}

1.5 Use-after-free

av_frame_free(&frame);
uint8_t v = frame->data[0][0];   // ⛔ 已释放, 崩溃

✅ free 后置 NULL:

av_frame_free(&frame);
frame = NULL;

2. 引用计数(Reference Counting)

2.1 为什么需要

AVFrame 可能被多个线程引用
  - 解码器持有
  - 显示队列持有
  - 录制队列持有

每个用完才 free → 引用计数

2.2 FFmpeg 引用计数

// 引用同一个 frame
AVFrame* f1 = av_frame_alloc();
// ... 解码 ...

AVFrame* f2 = av_frame_alloc();
av_frame_ref(f2, f1);   // f2 引用 f1 的数据 (refcount++)

// 用 f1
// 用 f2

av_frame_unref(f1);    // 引用计数 -1
av_frame_unref(f2);    // 计数到 0, 自动释放

av_frame_free(&f1);
av_frame_free(&f2);

2.3 C++ shared_ptr

auto frame = std::shared_ptr<AVFrame>(av_frame_alloc(), [](AVFrame* f) {
    av_frame_free(&f);
});

auto f2 = frame;   // 引用计数 +1, 自动管理
// 离开作用域 → 计数 -1, 到 0 自动 free

3. 字节序(Endianness)

3.1 大端 vs 小端

0x12345678 在内存里:

小端 (Little Endian, x86 / ARM 默认):
  地址 N+0: 0x78
  地址 N+1: 0x56
  地址 N+2: 0x34
  地址 N+3: 0x12

大端 (Big Endian, 网络字节序):
  地址 N+0: 0x12
  地址 N+1: 0x34
  地址 N+2: 0x56
  地址 N+3: 0x78

3.2 在音视频里何时遇到

RTMP / RTP 包: 大端 (网络字节序)
mp4 box header: 大端
PCM 音频:      小端 (主流)
H.264 NAL:    没字节序问题 (按字节读)

3.3 转换

#include <arpa/inet.h>

uint32_t host_value = 0x12345678;
uint32_t net_value = htonl(host_value);   // host → network (大端)
uint32_t back = ntohl(net_value);          // network → host

// 16 bit 版本
uint16_t htons(uint16_t);
uint16_t ntohs(uint16_t);

3.4 不转换的常见 bug

// 解析 mp4 box size (大端)
uint32_t size_le = *(uint32_t*)data;     // ⛔ 错, 直接读是小端
uint32_t size_be = ntohl(*(uint32_t*)data);   // ✅ 对

很多解析器 bug 都是这。


4. 内存对齐(Alignment)

4.1 SIMD 要求对齐

SSE / NEON 等 SIMD 指令要求数据 16 字节对齐
不对齐 → 性能下降 / 崩溃 (老 ARM)

4.2 FFmpeg 的对齐分配

// 不对齐 (普通 malloc)
uint8_t* buf = malloc(1024);

// 对齐 (FFmpeg 推荐, 32 字节对齐)
uint8_t* buf = av_malloc(1024);

// 32 字节对齐 (AVX 用)
uint8_t* buf = av_mallocz(1024);

4.3 stride 也是对齐结果

// 1920×1080 的 Y plane, 每行 1920 字节
// 但 FFmpeg 内部 stride 可能是 1984 (16 字节对齐)

frame->linesize[0]   // 实际行字节数 (包括 padding)
frame->width         // 真实宽度

写代码必须按 linesize 跨行,不是按 width


5. C 风格字符串与 std::string

5.1 C 风格

char filename[256] = "video.mp4";
size_t len = strlen(filename);

// 拼接 (危险, 容易溢出)
strcat(filename, ".bak");

// 安全拼接
snprintf(filename, sizeof(filename), "%s.bak", filename);

5.2 std::string(C++)

std::string path = "video.mp4";
path += ".bak";        // 安全
size_t len = path.length();
const char* c_str = path.c_str();   // 给 C 函数用

5.3 跨语言:JNI 字符串

JNIEXPORT void JNICALL func(JNIEnv* env, jobject obj, jstring jpath) {
    const char* path = (*env)->GetStringUTFChars(env, jpath, NULL);
    // ... 用 path ...
    (*env)->ReleaseStringUTFChars(env, jpath, path);   // ⚠️ 必须 release
}

参考第 0.3 篇 JNI。


6. 函数指针(Callback)

6.1 C 风格回调

typedef int (*decode_callback_t)(void* user, AVFrame* frame);

void register_callback(decode_callback_t cb, void* user_data) {
    g_callback = cb;
    g_user_data = user_data;
}

// 用户实现
int my_decode_callback(void* user, AVFrame* frame) {
    MyContext* ctx = (MyContext*)user;
    // 处理 frame
    return 0;
}

register_callback(my_decode_callback, &my_ctx);

6.2 C++ 现代风格

#include <functional>

void register_callback(std::function<int(AVFrame*)> cb) {
    g_callback = cb;
}

// 用 lambda
register_callback([this](AVFrame* f) {
    return this->process(f);
});

7. 多线程基础

7.1 pthread

#include <pthread.h>

void* worker(void* arg) {
    // 工作线程
    return NULL;
}

pthread_t tid;
pthread_create(&tid, NULL, worker, NULL);
pthread_join(tid, NULL);

7.2 互斥锁

pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;

pthread_mutex_lock(&mtx);
// 临界区
pthread_mutex_unlock(&mtx);

7.3 条件变量(生产者消费者)

pthread_mutex_t mtx;
pthread_cond_t cond;
Queue* q;

void producer() {
    pthread_mutex_lock(&mtx);
    queue_put(q, item);
    pthread_cond_signal(&cond);
    pthread_mutex_unlock(&mtx);
}

void consumer() {
    pthread_mutex_lock(&mtx);
    while (queue_empty(q)) {
        pthread_cond_wait(&cond, &mtx);
    }
    item = queue_get(q);
    pthread_mutex_unlock(&mtx);
}

7.4 C++11 标准库

#include <thread>
#include <mutex>
#include <condition_variable>

std::mutex mtx;
std::condition_variable cv;
std::queue<int> q;

void producer() {
    std::lock_guard<std::mutex> lock(mtx);
    q.push(item);
    cv.notify_one();
}

void consumer() {
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, [&]{ return !q.empty(); });
    item = q.front();
    q.pop();
}

8. 几个音视频专用技巧

8.1 ring buffer(环形缓冲区)

typedef struct {
    uint8_t* data;
    size_t size;
    size_t read_pos;
    size_t write_pos;
} RingBuffer;

void rb_write(RingBuffer* rb, const uint8_t* src, size_t len) {
    if (rb->write_pos + len <= rb->size) {
        memcpy(rb->data + rb->write_pos, src, len);
    } else {
        size_t first = rb->size - rb->write_pos;
        memcpy(rb->data + rb->write_pos, src, first);
        memcpy(rb->data, src + first, len - first);
    }
    rb->write_pos = (rb->write_pos + len) % rb->size;
}

音频应用最常用,避免 audio underrun 时数据回环

8.2 lock-free 队列

音视频高频数据流(每秒万次)用普通锁会卡。lock-free 单生产者单消费者:

typedef struct {
    void** items;
    atomic_int read_idx;
    atomic_int write_idx;
    int size;
} LockFreeQueue;

详细实现略,MediaCodec / WebRTC 内部都用 lock-free

8.3 SIMD 优化

#include <arm_neon.h>

// NEON 加速 YUV → RGB
void yuv_to_rgb_neon(uint8_t* y, uint8_t* u, uint8_t* v, uint8_t* rgb, int n) {
    for (int i = 0; i < n; i += 8) {
        uint8x8_t y_v = vld1_u8(y + i);
        // ... NEON 计算 ...
        vst3_u8(rgb + i*3, ...);
    }
}

ARM 上比纯 C 快 5-10 倍。FFmpeg 的 swscale 大量用 NEON。


9. 常见崩溃 / 内存错误调试

9.1 用 ASAN(AddressSanitizer)

# 编译时加
gcc -fsanitize=address -g demo.c -o demo

# 运行
./demo
# 任何内存错误会立即打印

ASAN 能抓到:

  • 缓冲区溢出
  • use-after-free
  • 内存泄漏
  • double-free

调试 C/C++ 必备

9.2 valgrind

valgrind --leak-check=full ./demo

更慢但更全面。

9.3 lldb / gdb

# 崩溃后:
lldb demo
> run
> bt   # 看堆栈

10. 几个 C/C++ 在音视频里的"惯用法"

10.1 错误码风格

int decode_frame(...) {
    if (...) return -1;   // 错误
    if (...) return AVERROR(EAGAIN);
    return 0;             // 成功
}

// 调用方:
int ret = decode_frame(...);
if (ret == AVERROR(EAGAIN)) { /* 重试 */ }
else if (ret < 0) { /* 错误 */ }
else { /* 成功 */ }

FFmpeg / MediaCodec 都是这风格。

10.2 上下文结构体(C 的"对象")

typedef struct {
    AVFormatContext* fmt;
    AVCodecContext* dec;
    int video_idx;
    pthread_t thread;
    // ... 几十个成员 ...
} Player;

Player* player_create();
int player_open(Player* p, const char* url);
void player_close(Player* p);

这是 FFmpeg / VLC 的标准风格——没有 class,但用结构体 + 函数实现"对象"

10.3 Opaque pointer(PIMPL)

// 头文件
typedef struct PlayerImpl Player;
Player* player_create();
void player_destroy(Player* p);

// 实现文件
struct PlayerImpl {
    // 内部细节
};

API 暴露不暴露内部成员,改实现不破坏 ABI。WebRTC / VLC 大量用。


11. 总结

音视频开发的 C/C++ 关键认知:

1. 内存管理 (malloc/free/RAII/refcount)
2. 字节序 (网络字节序 vs 主机字节序)
3. 内存对齐 (SIMD / stride)
4. 函数指针 + 上下文结构体 (C "对象")
5. 多线程 (pthread / std::thread / lock-free)
6. JNI / 跨语言数据传递 (参考 0.3 篇)

学习路径建议:

先会用: C 基础 + 指针 + 引用计数 + pthread
再深入: SIMD + lock-free + RAII + PIMPL
最后: ASAN + 调试技巧

金句:音视频开发的 C/C++ 不需要变成"语言专家"。但掌握指针 / 内存管理 / 字节序这 3 个,就能看懂 80% 的 FFmpeg 源码。

更多推荐