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告诉编译器该变量可能被外部因素改变,禁止优化。
使用场景:
- 硬件寄存器访问
#define GPIO_IDR (*((volatile uint32_t*)0x40020410))
void read_gpio() {
uint32_t value = GPIO_IDR; // 每次都从硬件读取
}
- 中断标志变量
volatile uint8_t data_ready = 0;
void UART_IRQHandler(void) {
data_ready = 1; // 中断中设置
}
int main() {
while (!data_ready); // 不加volatile会被优化成死循环
process_data();
}
- 多线程共享变量
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可执行文件
关键阶段:
- 预处理: 展开宏、处理#include
arm-none-eabi-gcc -E main.c -o main.i
- 编译: 生成汇编代码
arm-none-eabi-gcc -S main.i -o main.s
- 汇编: 生成机器码
arm-none-eabi-gcc -c main.s -o main.o
- 链接: 合并目标文件,解析符号
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);
网络协议和跨平台数据传输必须处理大小端问题。