HiHuo
首页
博客
手册
工具
关于
首页
博客
手册
工具
关于
  • 技术面试完全指南

    • 技术面试完全指南
    • 8年面试官告诉你:90%的简历在第一轮就被刷掉了
    • 刷了500道LeetCode,终于明白大厂算法面试到底考什么
    • 高频算法题精讲-双指针与滑动窗口
    • 03-高频算法题精讲-二分查找与排序
    • 04-高频算法题精讲-树与递归
    • 05-高频算法题精讲-图与拓扑排序
    • 06-高频算法题精讲-动态规划
    • Go面试必问:一道GMP问题,干掉90%的候选人
    • 08-数据库面试高频题
    • 09-分布式系统面试题
    • 10-Kubernetes与云原生面试题
    • 11-系统设计面试方法论
    • 前端面试高频题
    • AI 与机器学习面试题
    • 行为面试与软技能

C语言核心

指针与内存

指针类型与基础

指针是C语言的核心特性,本质是存储内存地址的变量。

int a = 10;
int *p = &a;        // p存储a的地址
int **pp = &p;      // pp存储p的地址(多级指针)

指针运算:

int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
p++;                // 移动到下一个int(地址+4字节)
*(p + 2);           // 访问arr[3]
p - arr;            // 计算偏移量

函数指针

函数指针用于回调机制和状态机实现:

typedef void (*callback_t)(int);

void event_handler(int event) {
    printf("Event: %d\n", event);
}

callback_t handler = event_handler;
handler(100);  // 调用函数

指针数组 vs 数组指针

// 指针数组:数组的每个元素都是指针
int *ptr_array[10];     // 10个int*指针的数组

// 数组指针:指向数组的指针
int (*array_ptr)[10];   // 指向包含10个int的数组的指针

内存布局

嵌入式系统的内存分为5个区域:

区域描述生命周期典型大小(STM32)
代码段(Text)存储程序代码永久Flash 128KB-2MB
数据段(Data)已初始化全局/静态变量永久RAM数KB
BSS段未初始化全局/静态变量永久RAM数KB
堆(Heap)动态分配(malloc)手动管理RAM可配置
栈(Stack)局部变量、函数调用自动管理RAM 2-8KB
int global_init = 100;      // Data段
int global_uninit;          // BSS段
static int static_var;      // BSS段

void func() {
    int local = 10;         // 栈
    static int s = 0;       // Data段
    char *p = malloc(100);  // p在栈,指向堆
    free(p);
}

内存泄漏检测

// 简单的内存跟踪
typedef struct {
    void *ptr;
    size_t size;
    const char *file;
    int line;
} MemInfo;

#define MALLOC(size) track_malloc(size, __FILE__, __LINE__)
#define FREE(ptr) track_free(ptr)

void* track_malloc(size_t size, const char *file, int line) {
    void *ptr = malloc(size);
    // 记录分配信息
    return ptr;
}

volatile关键字

volatile告诉编译器该变量可能被外部因素改变,禁止优化。

使用场景:

  1. 硬件寄存器访问
#define GPIO_IDR  (*((volatile uint32_t*)0x40020410))

void read_gpio() {
    uint32_t value = GPIO_IDR;  // 每次都从硬件读取
}
  1. 中断标志变量
volatile uint8_t data_ready = 0;

void UART_IRQHandler(void) {
    data_ready = 1;  // 中断中设置
}

int main() {
    while (!data_ready);  // 不加volatile会被优化成死循环
    process_data();
}
  1. 多线程共享变量
volatile int shared_counter = 0;

void Task1(void) {
    shared_counter++;
}

const关键字

常量指针 vs 指针常量

// 常量指针:指向的内容是常量
const int *p1 = &a;
*p1 = 10;    // 错误:不能修改内容
p1 = &b;     // 正确:可以修改指向

// 指针常量:指针本身是常量
int * const p2 = &a;
*p2 = 10;    // 正确:可以修改内容
p2 = &b;     // 错误:不能修改指向

// 常量指针常量
const int * const p3 = &a;

const在函数中的应用

// 参数保护
void print_data(const uint8_t *data, size_t len) {
    // data[0] = 0;  // 编译错误
    for (size_t i = 0; i < len; i++) {
        printf("%02X ", data[i]);
    }
}

// 返回值保护
const char* get_version(void) {
    return "v1.0.0";  // 防止调用者修改
}

static关键字

三种用法

// 1. 静态局部变量:保持值,但作用域限制在函数内
void counter() {
    static int count = 0;  // 只初始化一次
    count++;
    printf("%d\n", count);
}

// 2. 静态全局变量:限制作用域在文件内
static int file_private = 100;

// 3. 静态函数:限制作用域在文件内
static void internal_helper() {
    // 其他文件不可见
}

位操作

位操作是嵌入式开发的基础技能:

// 寄存器位操作宏
#define BIT_SET(reg, bit)     ((reg) |= (1U << (bit)))
#define BIT_CLR(reg, bit)     ((reg) &= ~(1U << (bit)))
#define BIT_TOGGLE(reg, bit)  ((reg) ^= (1U << (bit)))
#define BIT_READ(reg, bit)    (((reg) >> (bit)) & 1U)

// 实际应用:GPIO配置
uint32_t GPIOA_MODER = 0;
BIT_SET(GPIOA_MODER, 10);    // 设置PA5为输出(位10-11)
BIT_SET(GPIOA_MODER, 11);

// 掩码操作
#define UART_CR1_UE   (1 << 13)
#define UART_CR1_TE   (1 << 3)
#define UART_CR1_RE   (1 << 2)

USART1->CR1 |= (UART_CR1_UE | UART_CR1_TE | UART_CR1_RE);

实用位操作技巧:

// 判断奇偶
bool is_odd(int n) { return n & 1; }

// 交换两个数(无需临时变量)
void swap(int *a, int *b) {
    *a ^= *b;
    *b ^= *a;
    *a ^= *b;
}

// 计算二进制中1的个数
int count_bits(uint32_t n) {
    int count = 0;
    while (n) {
        count++;
        n &= (n - 1);  // 清除最低位的1
    }
    return count;
}

内存对齐

对齐原则

CPU访问对齐的数据更高效。对齐规则:

  • char: 1字节对齐
  • short: 2字节对齐
  • int: 4字节对齐
  • 结构体:最大成员的对齐要求
struct Example1 {
    char a;      // 1字节
    // 填充3字节
    int b;       // 4字节
    short c;     // 2字节
    // 填充2字节
};  // 总大小:12字节

struct Example2 {
    int b;       // 4字节
    short c;     // 2字节
    char a;      // 1字节
    // 填充1字节
};  // 总大小:8字节

紧凑结构体

struct __attribute__((packed)) SensorData {
    uint8_t id;
    uint32_t timestamp;
    uint16_t value;
};  // 大小:7字节(不对齐)

// 用于通信协议,但访问速度较慢

编译链接过程

源文件(.c) -> [预处理] -> .i文件 -> [编译] -> .s汇编 -> [汇编] -> .o目标文件 -> [链接] -> .elf可执行文件

关键阶段:

  1. 预处理: 展开宏、处理#include
arm-none-eabi-gcc -E main.c -o main.i
  1. 编译: 生成汇编代码
arm-none-eabi-gcc -S main.i -o main.s
  1. 汇编: 生成机器码
arm-none-eabi-gcc -c main.s -o main.o
  1. 链接: 合并目标文件,解析符号
arm-none-eabi-gcc main.o startup.o -T link.ld -o firmware.elf

C代码优化示例

// 嵌入式常用的循环展开
void memcpy_fast(uint8_t *dst, const uint8_t *src, size_t len) {
    size_t i;
    // 按4字节拷贝
    for (i = 0; i < len / 4; i++) {
        *((uint32_t*)dst) = *((uint32_t*)src);
        dst += 4;
        src += 4;
    }
    // 处理剩余字节
    for (i = 0; i < len % 4; i++) {
        *dst++ = *src++;
    }
}

// 查表法优化CRC计算
static const uint8_t crc8_table[256] = { /* ... */ };

uint8_t crc8(const uint8_t *data, size_t len) {
    uint8_t crc = 0xFF;
    for (size_t i = 0; i < len; i++) {
        crc = crc8_table[crc ^ data[i]];
    }
    return crc;
}

高频面试题

1. malloc/free的实现原理是什么?

答案:

malloc维护一个空闲链表(free list),记录可用内存块:

typedef struct Block {
    size_t size;
    struct Block *next;
    char data[1];  // 柔性数组
} Block;

void* my_malloc(size_t size) {
    // 遍历空闲链表
    Block *curr = free_list;
    while (curr) {
        if (curr->size >= size) {
            // 找到合适的块,分割并返回
            return curr->data;
        }
        curr = curr->next;
    }
    // 没有合适的块,向系统申请
    return sbrk(size + sizeof(Block));
}

free将内存块归还到空闲链表,相邻块会合并以减少碎片。

2. 如何检测栈溢出?

答案:

方法1:栈填充检测

#define STACK_CANARY 0xDEADBEEF
uint32_t stack_canary = STACK_CANARY;

void check_stack_overflow() {
    if (stack_canary != STACK_CANARY) {
        error_handler("Stack overflow detected!");
    }
}

方法2:使用MPU(内存保护单元)

// 配置栈区域为不可访问,溢出时触发HardFault
MPU->RNR = 0;  // Region 0
MPU->RBAR = STACK_GUARD_ADDR;
MPU->RASR = MPU_RASR_ENABLE | MPU_RASR_SIZE_4KB | MPU_RASR_AP_NOACCESS;

3. volatile和const能同时使用吗?

答案:

可以!const volatile用于只读硬件寄存器:

const volatile uint32_t* const STATUS_REG = (uint32_t*)0x40000000;

// 硬件可能改变值(volatile)
// 软件不能修改(const)
uint32_t status = *STATUS_REG;  // 正确
*STATUS_REG = 0;                // 编译错误

典型场景:读取ADC数据寄存器、状态寄存器等。

4. 如何实现不使用临时变量交换两个变量?

答案:

方法1:异或法

void swap_xor(int *a, int *b) {
    if (a != b) {  // 防止同一地址
        *a ^= *b;
        *b ^= *a;
        *a ^= *b;
    }
}

方法2:加减法

void swap_add(int *a, int *b) {
    *a = *a + *b;
    *b = *a - *b;
    *a = *a - *b;
}

注意:加减法可能溢出,异或法更安全。

5. 大小端如何判断和转换?

答案:

判断系统大小端:

bool is_little_endian() {
    uint32_t test = 0x12345678;
    return (*(uint8_t*)&test == 0x78);
}

转换函数:

uint32_t swap_endian32(uint32_t value) {
    return ((value & 0xFF000000) >> 24) |
           ((value & 0x00FF0000) >> 8)  |
           ((value & 0x0000FF00) << 8)  |
           ((value & 0x000000FF) << 24);
}

// 使用GCC内置函数(更高效)
uint32_t swap = __builtin_bswap32(value);

网络协议和跨平台数据传输必须处理大小端问题。