目录
  1. 1. 一、C 语言在 Android 中的不可替代性
  2. 2. 二、函数指针与回调
    1. 2.1. 2.1 函数指针数组——状态机
  3. 3. 三、内存布局与四区模型
  4. 4. 四、位运算与位域
  5. 5. 五、C 预处理器宏
  6. 6. 六、setjmp/longjmp 与异常处理
  7. 7. 七、pthread 线程基础
  8. 8. 八、面试常问题目
【C/C++理论实战技术】BAT最常用的C技术

一、C 语言在 Android 中的不可替代性

尽管 Android NDK 开发越来越多地使用 C++,C 语言仍然是系统编程的基石:

  1. Linux 内核全部用 C 编写。Android 基于 Linux 内核,内核模块和驱动都是 C。
  2. Android 的 Bionic libc 是用 C 实现的。所有系统调用(open、read、write、mmap、ioctl 等)的封装都是 C。
  3. 性能关键路径。ART 运行时的垃圾回收(GC)、JIT 编译器的代码生成、HAL 层的硬件通信,都是 C/汇编。
  4. ABI 稳定性。C 的 ABI 比 C++ 稳定得多。JNI 的接口是 C 接口(JNIEnv 是 C 函数指针表)。跨语言 FFI(Foreign Function Interface)几乎都基于 C ABI。
  5. 二进制体积。C 运行时极简(Android 的 libc.so 约 500KB),而 C++ 的 STL 会增加显著体积。

AOSP 源码中 bionic/ 目录(Android 的 C 库)和 system/core/ 中的大量工具都用 C 编写。

二、函数指针与回调

函数指针是 C 语言实现多态和回调的基础机制:

#include <stdio.h>

// 定义函数指针类型
typedef int (*compare_func_t)(const void *, const void *);

// 通用排序函数——通过函数指针实现策略模式
void sort(void *base, size_t nmemb, size_t size, compare_func_t cmp) {
// qsort 的内部调用 cmp 进行元素比较
}

// 具体比较函数
int compare_int(const void *a, const void *b) {
return (*(int *)a - *(int *)b);
}

int compare_string(const void *a, const void *b) {
return strcmp(*(const char **)a, *(const char **)b);
}

// 使用
int main() {
int arr[] = {5, 2, 8, 1, 3};
sort(arr, 5, sizeof(int), compare_int);

char *strs[] = {"banana", "apple", "cherry"};
sort(strs, 3, sizeof(char *), compare_string);
return 0;
}

在 Android 的 JNI 环境中,函数指针常用于 native 层的回调注册:

// 注册一个 native 回调(类似观察者模式)
typedef void (*on_data_received_cb)(const uint8_t *data, size_t len, void *user_data);

typedef struct {
on_data_received_cb callback;
void *user_data;
} DataListener;

void register_listener(DataListener *listener, on_data_received_cb cb, void *user_data) {
listener->callback = cb;
listener->user_data = user_data;
}

// 数据到达时调用
void on_packet_arrived(DataListener *listener, const uint8_t *packet, size_t len) {
if (listener->callback) {
listener->callback(packet, len, listener->user_data);
}
}

2.1 函数指针数组——状态机

// 状态机中的状态转移表
typedef enum {
STATE_IDLE,
STATE_CONNECTING,
STATE_CONNECTED,
STATE_CLOSING,
} ConnectionState;

typedef void (*state_handler_t)(void *context);

// 状态处理函数表
static state_handler_t state_handlers[] = {
[STATE_IDLE] = handle_idle,
[STATE_CONNECTING] = handle_connecting,
[STATE_CONNECTED] = handle_connected,
[STATE_CLOSING] = handle_closing,
};

void dispatch_state(ConnectionState state, void *context) {
if (state < sizeof(state_handlers) / sizeof(state_handlers[0])) {
state_handlers[state](context); // 通过函数指针派发
}
}

三、内存布局与四区模型

理解 C 语言的内存布局是调试 native crash 和内存相关 bug 的基础。Android 进程的内存空间(虚拟地址空间)布局如下:

高地址
┌──────────────────┐
│ Kernel Space │ (3GB-4GB in 32-bit, vast in 64-bit)
├──────────────────┤
│ Stack (↓下增) │ 局部变量、函数返回地址
│ ... │
│ ↓ │
│ │
│ ↑ │
│ ... │
│ Heap (↑上增) │ malloc / free
├──────────────────┤
│ BSS (未初始化) │ 未初始化/零初始化的全局变量和静态变量
├──────────────────┤
│ Data (已初始化) │ 已初始化的全局变量和静态变量
├──────────────────┤
│ Text (代码段) │ 可执行指令(只读)
└──────────────────┘
低地址

四区模型

区域 存储内容 生命周期 管理方式
(Stack) 局部变量、函数参数、返回地址 函数返回时自动销毁 编译器自动管理
(Heap) malloc/calloc/realloc 分配的内存 手动 free 或进程退出 程序员显式管理
静态区(Data/BSS) 全局变量、static 变量 程序整个运行期 编译器分配
代码区(Text) 可执行指令、只读数据(字符串常量) 程序整个运行期 只读
// 各区域的实际示例
int global_initialized = 42; // Data 段
int global_uninitialized; // BSS 段
static int static_var = 100; // Data 段

const char *msg = "Hello"; // msg 在 Data 段,"Hello" 在 Text 段(只读)

void demo() {
int local = 10; // 栈
static int counter = 0; // Data 段(但只在函数内可见)
counter++;

char *heap_mem = (char *)malloc(1024); // 堆
// ... 使用 heap_mem ...
free(heap_mem); // 必须显式释放
}

在 Android NDK 开发中,常见的崩溃类型与内存布局直接相关:

  • Stack Overflow:递归太深或局部数组过大(Android 默认线程栈大小约 1MB)。
  • Heap Corruption:double free、use-after-free、buffer overflow。
  • Segmentation Fault(SIGSEGV):访问 NULL 指针、访问已释放的内存、写只读内存。

四、位运算与位域

C 语言的位运算是嵌入式开发和协议解析的利器。Android 的 Binder 协议、HAL 接口标志位、编解码器的比特流操作都大量使用位运算。

// 常见位运算模式
#define FLAG_A (1 << 0) // 0x01
#define FLAG_B (1 << 1) // 0x02
#define FLAG_C (1 << 2) // 0x04

uint32_t flags = 0;

// 设置标志
flags |= FLAG_A; // 设置 FLAG_A
flags |= (FLAG_B | FLAG_C); // 同时设置多个

// 清除标志
flags &= ~FLAG_A; // 清除 FLAG_A

// 测试标志
if (flags & FLAG_B) { } // FLAG_B 是否置位

// 切换标志
flags ^= FLAG_C; // 翻转 FLAG_C

// 提取 bit 范围(如提取 bit 4-7)
uint8_t extracted = (flags >> 4) & 0x0F;

// 对齐到 4 字节边界
size_t aligned = (size + 3) & ~3;

位域(Bit Field)可以更自然地表达硬件寄存器和协议头:

// IPv4 包头(20 字节,无 Options)
struct ip_header {
uint8_t version_ihl; // version(4bit) + IHL(4bit)
uint8_t dscp_ecn; // DSCP(6bit) + ECN(2bit)
uint16_t total_length;
uint16_t identification;
uint16_t flags_fragment; // flags(3bit) + fragment offset(13bit)
uint8_t ttl;
uint8_t protocol;
uint16_t header_checksum;
uint32_t source_ip;
uint32_t dest_ip;
};

// 使用位域更精确
struct ip_header_bitfield {
unsigned int ihl : 4; // 0-3 bit
unsigned int version : 4; // 4-7 bit
unsigned int ecn : 2; // 8-9 bit
unsigned int dscp : 6; // 10-15 bit
// ...
};

#define IP_VERSION(hdr) (((hdr)->version_ihl & 0xF0) >> 4)
#define IP_IHL(hdr) ((hdr)->version_ihl & 0x0F)

五、C 预处理器宏

宏是 C 语言元编程的手段。在 Android 底层代码中非常常见:

// 1. 字符串化(#)和连接(##)
#define MAKE_FUNC(name) native_##name
// MAKE_FUNC(encrypt) → native_encrypt

#define STRINGIFY(x) #x
#define TO_STRING(x) STRINGIFY(x)
// TO_STRING(42) → "42"

// 2. do-while(0) 惯用法——让宏像函数一样工作
#define SAFE_FREE(p) do { \
if ((p) != NULL) { \
free(p); \
(p) = NULL; \
} \
} while (0)
// 注意分号由调用者提供

// 3. 调试宏
#ifdef DEBUG
#define LOG_TAG "NativeLib"
#include <android/log.h>
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
#else
#define LOGD(...) ((void)0)
#define LOGW(...) ((void)0)
#define LOGE(...) ((void)0)
#endif

// 4. 编译期断言
#define STATIC_ASSERT(cond) _Static_assert(cond, #cond)
STATIC_ASSERT(sizeof(int) == 4); // 编译期检查

六、setjmp/longjmp 与异常处理

C 语言没有像 C++ 的 try-catch 异常机制,但 setjmp/longjmp 提供了非本地跳转(non-local goto),可以实现异常安全:

#include <setjmp.h>

static jmp_buf g_error_jmp;

void process_data(const char *filename) {
// 保存当前执行上下文
if (setjmp(g_error_jmp) != 0) {
// longjmp 跳转到此处(返回值非 0)
printf("Error occurred during processing, rolling back...\n");
rollback_transaction();
return;
}

// 正常处理流程
open_file(filename); // 可能调用 longjmp
parse_header();
process_body();
}

// 在深层调用的函数中出错时
void parse_header() {
if (header_corrupted()) {
// 跳回 setjmp 点,参数是返回值
longjmp(g_error_jmp, 1);
}
}

实际中,Android 的 libpng(PNG 解码库)、libjpeg-turbo(JPEG 编解码器)等 C 库内部使用 setjmp/longjmp 实现错误处理。当解码失败时,从深层递归/循环中直接跳回错误处理点,避免了逐层返回和检查返回值的繁琐。

注意:longjmp 不会调用 C++ 对象的析构函数(不会栈展开),所以在 C++ 中使用需要特别小心。在 C 中,使用 setjmp/longjmp 时需要确保不会泄漏已分配的资源(通常在 setjmp 调用处维护一个资源列表用于回滚)。

七、pthread 线程基础

Android 的 Bionic 完整支持 POSIX 线程(pthread)。虽然 Android NDK 可以使用 C++11 的 std::thread,但 pthread 仍然是系统级线程操作的基础:

#include <pthread.h>
#include <android/log.h>

typedef struct {
int thread_id;
const char *name;
void *(*start_routine)(void *);
void *arg;
} ThreadParams;

static void *thread_entry(void *arg) {
ThreadParams *params = (ThreadParams *)arg;

// 在 Android 中设置线程名(方便调试)
pthread_setname_np(pthread_self(), params->name);

__android_log_print(ANDROID_LOG_INFO, "NativeThread",
"Thread %d '%s' started", params->thread_id, params->name);

void *result = params->start_routine(params->arg);

__android_log_print(ANDROID_LOG_INFO, "NativeThread",
"Thread %d '%s' finished", params->thread_id, params->name);

return result;
}

int create_thread(ThreadParams *params) {
pthread_t thread;
pthread_attr_t attr;

pthread_attr_init(&attr);
// 设置为 detached:线程结束后资源自动回收(不需要 pthread_join)
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

int ret = pthread_create(&thread, &attr, thread_entry, params);
pthread_attr_destroy(&attr);
return ret;
}

// 互斥锁和条件变量
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
static int ready = 0;

void producer() {
pthread_mutex_lock(&mutex);
// 生产数据...
ready = 1;
pthread_cond_signal(&cond); // 通知消费者
pthread_mutex_unlock(&mutex);
}

void consumer() {
pthread_mutex_lock(&mutex);
while (!ready) {
pthread_cond_wait(&cond, &mutex); // 等待通知(自动释放 mutex)
}
// 消费数据...
pthread_mutex_unlock(&mutex);
}

八、面试常问题目

Q1: 栈(Stack)和堆(Heap)的区别?在 Android NDK 中如何选择?

栈由编译器自动管理,分配极快(移动栈指针),函数返回时自动回收,但空间有限(Android 中每个线程约 1MB 默认栈大小)。堆由 malloc/free 手动管理,分配较慢(需要查找空闲块),空间大(受限于进程虚拟地址空间),但容易内存泄漏和碎片化。规则:小的固定大小对象放栈上,大的动态大小对象放堆上。Android NDK 中大型数组(如解码后的图片缓冲区)必须用堆分配。

Q2: 函数指针和回调的典型应用场景?

函数指针在 C 中用于实现策略模式和多态:(1) 排序/查找算法的比较函数(qsort、bsearch);(2) 事件驱动的回调注册(JNI 回调 Java 方法本质就是函数指针+JNI 环境);(3) 状态机的状态转移表;(4) 插件系统的接口抽象(动态库加载通过 dlsym 获取函数指针);(5) 信号处理函数(signal handler)。

Q3: C 的宏和 C++ 的模板有什么区别?

宏是预处理器文本替换,没有类型检查,容易产生边界效应(需要用括号保护参数和整体),调试困难(编译错误指向展开后的代码)。模板是编译器层面的代码生成,有类型检查,生成的代码经过了完整的语义分析,错误信息更有意义。宏可以生成任意的代码文本(包括控制流和声明),模板主要适用于泛型算法和类型安全的容器。在 C 语言中宏是唯一的代码生成手段,在 C++ 中优先使用模板。

Q4: Android Bionic libc 和 glibc 有什么区别?

Bionic 是 Android 定制的 C 库(源码路径:bionic/libc/),专为移动设备优化。主要区别:(1) Bionic 体积更小(~500KB vs glibc 的几 MB);(2) Bionic 没有完整的 locale 支持;(3) Bionic 的 pthread 实现简化(基于 futex 而非 NPTL);(4) Bionic 对一些 POSIX 函数没有实现或有限制(如 system() 在某些 Android 版本被限制);(5) Bionic 的 DNS 解析集成到 netd 守护进程。这些区别使得一些 Linux 程序移植到 Android 时需要额外适配。


参考源码路径:

  • Bionic libc:bionic/libc/
  • Bionic pthread:bionic/libc/bionic/pthread_create.cpp
  • Bionic malloc (jemalloc/scudo):bionic/libc/bionic/jemalloc_wrapper.cpp
  • Linux 内核内存管理:kernel/msm-*/mm/
  • AOSP 原生工具:system/core/toolbox/
  • Binder 协议定义:frameworks/native/libs/binder/
打赏
  • 微信
  • 支付宝

评论