目录
  1. 1. 一、C 语言在 Android 中的不可替代性
  2. 2. 二、指针深度剖析
    1. 2.1. 2.1 指针的底层模型
    2. 2.2. 2.2 void* 通用指针
    3. 2.3. 2.3 函数指针与回调
    4. 2.4. 2.4 函数指针数组——状态机
    5. 2.5. 2.5 指针运算与数组
  3. 3. 三、内存布局与四区模型
    1. 3.1. 3.1 结构体对齐与填充
    2. 3.2. 3.2 联合体(union)
  4. 4. 四、位运算与位域
  5. 5. 五、C 预处理器宏
    1. 5.1. 5.1 宏的陷阱
  6. 6. 六、setjmp/longjmp 与异常处理
    1. 6.1. 6.1 setjmp/longjmp 的实现原理
  7. 7. 七、pthread 线程基础
    1. 7.1. 7.1 pthread 同步深入
  8. 8. 八、常见安全漏洞与防护
    1. 8.1. 8.1 缓冲区溢出(Buffer Overflow)
    2. 8.2. 8.2 Use-After-Free(释放后使用)
    3. 8.3. 8.3 格式字符串漏洞
    4. 8.4. 8.4 整数溢出
  9. 9. 九、C11/C17 新特性
  10. 10. 十、面试常问题目
【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 语言最具威力也最容易被误用的特性。理解指针是掌握 C 语言进阶的关键。

2.1 指针的底层模型

指针本质上是一个存储内存地址的变量。在 32 位系统上占 4 字节,64 位系统上占 8 字节。但指针不仅仅是地址——它还带有类型信息,告诉编译器如何解释指向的内存。

int x = 0x12345678;
int *p = &x; // p 指向 x
char *cp = (char *)&x; // cp 也指向 x,但以 char 视角解读

// 在小端(little-endian)机器上:
// *cp == 0x78(最低有效字节)
// *(cp + 1) == 0x56
// *(cp + 2) == 0x34
// *(cp + 3) == 0x12

2.2 void* 通用指针

void* 是 C 语言中的”万能指针”,可以指向任何类型,但不能直接解引用——必须先转换为具体类型指针。这正是 malloc 返回 void* 的原因:分配的内存可以用于任何类型。

#include <stdlib.h>
#include <string.h>

// 通用 swap:通过 void* 实现类型无关的内存交换
void swap(void *a, void *b, size_t size) {
void *tmp = malloc(size);
if (tmp == NULL) return;
memcpy(tmp, a, size);
memcpy(a, b, size);
memcpy(b, tmp, size);
free(tmp);
}

// 使用示例
int a = 10, b = 20;
swap(&a, &b, sizeof(int)); // a=20, b=10

double x = 1.5, y = 3.7;
swap(&x, &y, sizeof(double)); // x=3.7, y=1.5

void* 的一个关键注意事项:对 void* 的指针运算在标准 C 中是未定义行为(GCC 作为扩展允许,将其视为 char* 运算)。正确做法是先转换为 char*

// BAD: 不可移植
void *ptr = malloc(100);
void *next = ptr + 10; // 标准 C 中未定义

// GOOD: 使用 char* 做指针运算
void *ptr = malloc(100);
void *next = (char *)ptr + 10; // 前移 10 字节

2.3 函数指针与回调

函数指针是 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.4 函数指针数组——状态机

// 状态机中的状态转移表
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); // 通过函数指针派发
}
}

2.5 指针运算与数组

// 数组名在表达式中退化为指向首元素的指针
int arr[10];
int *p = arr; // &arr[0]
int (*pa)[10] = &arr; // 指向整个数组的指针

// 指针减法:返回两指针间的元素个数(不是字节数)
int *p1 = &arr[5];
int *p2 = &arr[2];
ptrdiff_t diff = p1 - p2; // = 3(3 个 int 元素)

// 指针比较:只有指向同一数组(或数组末尾后一个位置)的指针比较才有意义
if (p1 > p2) { /* 合法 */ }

// 多维数组与指针
int matrix[3][4];
int (*row_ptr)[4] = matrix; // row_ptr 指向每行(4 个 int)
// row_ptr + 1 跳过一整行(4 个 int)

三、内存布局与四区模型

理解 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 指针、访问已释放的内存、写只读内存。

3.1 结构体对齐与填充

结构体在内存中的布局不是简单的字段相加,编译器会插入填充字节(padding)以满足对齐要求:

#include <stddef.h>

// 未优化的结构体
struct BadLayout {
char a; // 1 byte,对齐到 1
// 3 bytes padding(为了使 int b 对齐到 4 字节边界)
int b; // 4 bytes,对齐到 4
char c; // 1 byte,对齐到 1
// 3 bytes padding(为了使整个结构体大小对齐到最大成员的对齐边界)
};
// sizeof(struct BadLayout) = 12

// 优化后的结构体(按对齐要求降序排列)
struct GoodLayout {
int b; // 4 bytes
char a; // 1 byte
char c; // 1 byte
// 2 bytes padding
};
// sizeof(struct GoodLayout) = 8

// 编译期检查对齐
_Static_assert(sizeof(struct GoodLayout) == 8, "Unexpected padding");

// offsetof 宏查看成员偏移
printf("offset of a: %zu\n", offsetof(struct BadLayout, a)); // 0
printf("offset of b: %zu\n", offsetof(struct BadLayout, b)); // 4
printf("offset of c: %zu\n", offsetof(struct BadLayout, c)); // 8

Android NDK 中的实际影响:当 native 代码与 Java 层通过 JNI 传递结构体时,如果结构体布局不一致(如 native 和 Java 解析的偏移不同),会导致数据错乱。使用 __attribute__((packed)) 可以禁用对齐填充,但会降低内存访问性能(非对齐访问在某些 ARM 架构上会触发 SIGBUS)。

3.2 联合体(union)

联合体的所有成员共享同一块内存区域,大小为最大成员的大小。在 Android 底层开发中,联合体常用于协议解析和类型双关:

// IPv4 地址的双关访问
union IPv4 {
uint32_t addr; // 32-bit 整数
uint8_t octets[4]; // 4 个字节
struct {
uint8_t a, b, c, d; // 命名访问
};
};

union IPv4 ip;
ip.addr = 0x0A000001; // 10.0.0.1
printf("%d.%d.%d.%d\n", ip.octets[3], ip.octets[2],
ip.octets[1], ip.octets[0]); // 小端: 10.0.0.1

// 浮点数的位级表示
union FloatInspector {
float f;
uint32_t bits;
struct {
uint32_t mantissa : 23;
uint32_t exponent : 8;
uint32_t sign : 1;
};
};

四、位运算与位域

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;

// 判断是否为 2 的幂
int is_power_of_two(unsigned int x) {
return x && !(x & (x - 1));
}

// 统计二进制中 1 的个数(Brian Kernighan 算法)
int popcount(unsigned int x) {
int count = 0;
while (x) {
x &= (x - 1); // 每次清除最低位的 1
count++;
}
return count;
}

位域(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)

位域的可移植性注意事项:位域的内存布局(从 LSB 还是 MSB 开始分配)是编译器实现定义的,跨平台通信时不应该在位域结构体上直接做 memcpy——应该手动编码/解码到字节序列。

五、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); // 编译期检查

// 5. typeof / container_of(从成员指针推导父结构体)
#define container_of(ptr, type, member) \
((type *)((char *)(ptr) - offsetof(type, member)))

struct Node {
int value;
struct Node *next;
struct Node *prev;
};

void remove_node(struct Node *node) {
// 已知 node 是 MyStruct 的成员,通过 container_of 获取父结构体
// MyStruct *parent = container_of(node, MyStruct, node);
node->prev->next = node->next;
node->next->prev = node->prev;
}

5.1 宏的陷阱

// 陷阱 1:多次求值
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int x = 5;
int y = MAX(x++, 10); // 展开: ((x++) > (10) ? (x++) : (10))
// x++ 可能被执行两次!x 最终等于 7(而不是预期的 6)

// 解决方案:GCC 扩展 typeof / __auto_type
#define MAX_SAFE(a, b) ({ \
__auto_type _a = (a); \
__auto_type _b = (b); \
_a > _b ? _a : _b; \
})

// 陷阱 2:宏的嵌套展开
#define DOUBLE(x) (x * 2)
#define SQUARE(x) (x * x)
int result = SQUARE(DOUBLE(3)); // (DOUBLE(3) * DOUBLE(3)) = ((3 * 2) * (3 * 2)) = 36
// 看起来没问题,但更复杂的嵌套可能导致意外

六、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 调用处维护一个资源列表用于回滚)。

6.1 setjmp/longjmp 的实现原理

setjmp 保存的上下文通常包括:

  • 程序计数器(PC / IP):当前执行位置
  • 栈指针(SP)
  • 帧指针(FP)
  • 所有 callee-saved 寄存器(在 ARM64 上为 x19-x28)
  • 信号掩码(如果使用 sigsetjmp)

longjmp 恢复这些寄存器值,使执行流”跳回”到 setjmp 处。由于不进行栈展开,longjmp 的性能远优于 C++ 异常——但安全性和正确性完全由开发者保证。

// 多级错误码返回
volatile int error_code = 0;
static jmp_buf jmp;

if ((error_code = setjmp(jmp)) != 0) {
switch (error_code) {
case 1: handle_io_error(); break;
case 2: handle_parse_error(); break;
case 3: handle_memory_error(); break;
}
return;
}

longjmp(jmp, 2); // 触发 parse_error 处理

七、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);
}

7.1 pthread 同步深入

// 自旋锁(spinlock):适用于极短临界区,忙等而非睡眠
pthread_spinlock_t spin;
pthread_spin_init(&spin, PTHREAD_PROCESS_PRIVATE);
pthread_spin_lock(&spin);
// 极短的临界区操作
pthread_spin_unlock(&spin);
pthread_spin_destroy(&spin);

// 读写锁(rwlock):允许多读单写
pthread_rwlock_t rwlock;
pthread_rwlock_init(&rwlock, NULL);
// 读锁——多个线程可同时持有
pthread_rwlock_rdlock(&rwlock);
// 读操作...
pthread_rwlock_unlock(&rwlock);
// 写锁——独占
pthread_rwlock_wrlock(&rwlock);
// 写操作...
pthread_rwlock_unlock(&rwlock);
pthread_rwlock_destroy(&rwlock);

// 屏障(barrier):N 个线程全部到达后同时继续
pthread_barrier_t barrier;
pthread_barrier_init(&barrier, NULL, 4); // 4 个线程
// 在每个线程中:
pthread_barrier_wait(&barrier); // 阻塞直到 4 个线程都到达
pthread_barrier_destroy(&barrier);

八、常见安全漏洞与防护

8.1 缓冲区溢出(Buffer Overflow)

经典的 C 安全漏洞。Android 的 Stagefright 漏洞系列中多个都是基于缓冲区溢出。

// VULNERABLE: 无边界检查
void unsafe_copy(char *input) {
char buffer[64];
strcpy(buffer, input); // 如果 input 长度 > 63,溢出!
}

// SAFE: 使用带长度限制的函数
void safe_copy(char *input) {
char buffer[64];
strncpy(buffer, input, sizeof(buffer) - 1);
buffer[sizeof(buffer) - 1] = '\0'; // 确保 null 终止
}

// 更好的方案:使用 snprintf
snprintf(buffer, sizeof(buffer), "%s", input);

8.2 Use-After-Free(释放后使用)

// VULNERABLE: 使用已释放的内存
struct Connection *conn = create_connection();
process(conn);
free(conn);
conn->send_data(); // USE-AFTER-FREE! 崩溃或被利用

// SAFE: 释放后置 NULL + 使用前检查
free(conn);
conn = NULL; // 把指针置空
if (conn != NULL) { // 防御式检查
conn->send_data();
}

8.3 格式字符串漏洞

// VULNERABLE: 用户控制的格式字符串
void log_user_input(char *user_input) {
printf(user_input); // 如果输入包含 %n,可以写入任意地址!
}

// SAFE: 使用固定格式字符串
void log_user_input(char *user_input) {
printf("%s", user_input); // 或者用 puts()
}

8.4 整数溢出

// VULNERABLE
void allocate_buffer(size_t count, size_t elem_size) {
size_t total = count * elem_size;
// 如果 count * elem_size 溢出,total 可能是很小的值
char *buf = malloc(total); // 分配的内存不足
// ... 后续写入会溢出
}

// SAFE
void allocate_buffer(size_t count, size_t elem_size) {
if (count > 0 && elem_size > SIZE_MAX / count) {
// 溢出检测
return;
}
size_t total = count * elem_size;
char *buf = malloc(total);
}

九、C11/C17 新特性

Android NDK 支持 GCC 和 Clang,后者对 C 标准的支持更完备。以下是一些值得使用的现代 C 特性:

// 1. <stdatomic.h>:原子操作
#include <stdatomic.h>
atomic_int counter = ATOMIC_VAR_INIT(0);
int old = atomic_fetch_add(&counter, 1); // 原子递增
int val = atomic_load(&counter); // 原子读取

// 2. <threads.h>:标准线程(C11,Bionic 部分支持)
// Bionic 中 <threads.h> 基于 pthread 实现
#include <threads.h>
thrd_t thread;
thrd_create(&thread, thread_func, NULL);
thrd_join(thread, NULL);

// 3. anonymous struct/union(匿名结构体/联合体)
struct Person {
char *name;
union { // 匿名联合
int age;
int birth_year;
};
};
// 直接访问: p.age 或 p.birth_year(无需 p.u.age)

// 4. aligned_alloc:对齐内存分配
void *aligned_ptr = aligned_alloc(64, 1024); // 64 字节对齐的 1024 字节
free(aligned_ptr);

十、面试常问题目

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

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

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 时需要额外适配。

Q5: 结构体中的 padding 是什么?如何避免不必要的 padding?如何强制紧凑布局?

Padding 是编译器为了满足硬件对齐要求在结构体成员之间或末尾插入的填充字节。减少 padding 的策略:(1) 按照对齐要求从大到小排列成员(double/long long 在前,char/short 在后);(2) 将相同宽度的成员聚合在一起;(3) 如果需要强制紧凑布局(如与硬件寄存器或网络协议匹配),使用 __attribute__((packed))#pragma pack(1),但要注意这会导致非对齐访问——在某些 ARM 架构(如 ARMv5)上非对齐访问会引发异常,在 ARMv7+ 上虽然硬件支持但性能降低。


参考源码路径:

  • 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/
打赏
  • 微信
  • 支付宝

评论