指针与数组的高级用法:函数指针在状态机中的应用、数组指针与指针数组、const关键字的正确使用

好,咱们今天聊点硬核的。指针和数组,C语言里绕不开的两座大山。很多嵌入式工程师一提到指针就头疼,其实说白了,指针就是个地址,没那么玄乎。但真正用好它,尤其是在ADAS这种对实时性和可靠性要求极高的场景下,确实需要一些技巧。

我个人习惯把指针分为三类来理解:普通指针、数组指针、指针数组。这三者搞混了,代码跑飞是迟早的事。咱们一个一个说。

一、指针数组与数组指针:别搞混了

先看两个定义:

int *p1[10];   // 指针数组:一个数组,里面存了10个int*指针
int (*p2)[10]; // 数组指针:一个指针,指向一个含有10个int的数组

嗯,这里要注意。括号的位置决定了天壤之别。我在项目中遇到过一位同事,把数组指针当指针数组用,结果数据错位,查了三天bug。你想想看,ADAS系统里传感器数据流一旦错位,后果是什么?

指针数组,说白了就是数组里每个元素都是指针。常用于管理多个字符串,或者多个动态分配的内存块。比如:

const char *sensor_names[] = {
    "Camera",
    "Radar",
    "Lidar",
    "Ultrasonic"
};

这里sensor_names就是一个指针数组,每个元素指向一个字符串常量。

数组指针,则常用于二维数组的行操作。比如你要把一帧图像数据(假设是640x480的灰度图)传给某个函数处理:

void process_frame(int (*frame)[640], int rows) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < 640; j++) {
            // 处理每个像素
            frame[i][j] = clamp(frame[i][j], 0, 255);
        }
    }
}

这样传参,语义清晰,效率也高。数组指针保留了二维数组的步长信息,编译器能生成更优的寻址代码。

核心区别一句话:

  • 指针数组:数组里装的是指针
  • 数组指针:指针指向一个数组

二、函数指针在状态机中的应用

函数指针,这是嵌入式C语言里最优雅的设计之一。尤其是在ADAS系统的状态机实现中,用函数指针能让代码结构清晰到令人发指。

为什么这么说?你想想看,ADAS系统有多个状态:初始化、自检、待机、主动控制、故障处理...每个状态下,系统对输入事件的响应完全不同。如果用switch-case写,代码会膨胀成什么样?

// 传统switch-case方式(不推荐)
void state_machine(event_t evt) {
    switch(current_state) {
        case INIT:
            if (evt == EVT_SELF_CHECK_OK) {
                // 跳转到待机
                current_state = STANDBY;
            }
            break;
        case STANDBY:
            if (evt == EVT_ACTIVATE) {
                // 跳转到主动控制
                current_state = ACTIVE;
            }
            break;
        // ... 每个状态都要写一遍
    }
}

这种写法,状态一多,代码就变成一坨。而且每次新增状态,都要修改这个函数,违反了开闭原则。

用函数指针就清爽多了:

// 定义状态处理函数类型
typedef void (*state_handler_t)(event_t);

// 每个状态对应一个处理函数
void state_init(event_t evt) {
    if (evt == EVT_SELF_CHECK_OK) {
        current_state = STANDBY;
        current_handler = state_standby;  // 切换处理函数
    }
}

void state_standby(event_t evt) {
    if (evt == EVT_ACTIVATE) {
        current_state = ACTIVE;
        current_handler = state_active;
    }
}

// 状态机主循环
state_handler_t current_handler = state_init;

void run_state_machine(event_t evt) {
    if (current_handler) {
        current_handler(evt);
    }
}

你看,主循环就一行调用。新增状态只需要写一个新的处理函数,然后在合适的地方把current_handler切过去就行。我曾经在一个ADAS项目中用这种方式实现了12个状态的切换,代码量减少了40%,而且每个状态的处理逻辑完全隔离,调试起来特别爽。

我的经验:函数指针表(跳转表)配合状态机,是嵌入式系统里最经典的组合。把状态转移关系定义成二维表,查表执行,性能极高,适合实时性要求苛刻的ADAS场景。

三、const关键字的正确使用

const这个关键字,很多人觉得简单,不就是定义常量吗?其实它的用法远比想象中丰富。在ADAS开发中,const用得好,能避免大量低级bug。

const的几种常见用法:

写法 含义 典型场景
const int *p 指针指向的值不可修改 读取传感器数据,不修改
int * const p 指针本身不可修改 固定映射地址,如寄存器地址
const int * const p 指针和指向的值都不可修改 查表数据,如标定表

嗯,这里要注意。const的位置决定了保护的对象。我见过太多人把const int *pint * const p搞混,结果在ADAS的标定数据表里意外修改了常量,导致系统行为异常。

举个例子,ADAS系统里经常有标定参数表:

// 标定参数表,运行时绝不允许修改
const int calibration_table[256] = {
    0, 10, 20, 30, /* ... */
};

// 函数参数声明为const,明确告诉调用者:我不会改你的数据
void process_sensor_data(const uint8_t *data, uint32_t len) {
    for (uint32_t i = 0; i < len; i++) {
        // data[i] = 0;  // 编译报错!防止意外修改
        uint8_t val = data[i];  // 只读访问,安全
    }
}

我曾经在调试一个车道线检测算法时,发现输出结果偶尔异常。查了两天,最后发现是一个子函数里不小心修改了输入的图像数据缓冲区。从那以后,我要求团队所有只读参数的函数,必须加const修饰。编译器会帮你拦住这种低级错误。

避坑指南:我曾经在RTOS的任务间共享数据时,忘记给只读数据加const。结果一个任务意外修改了另一个任务的配置参数,导致系统在高速行驶时突然退出自适应巡航。嗯,从那以后,我养成了「能加const就加const」的习惯。const不仅是语法,更是一种设计契约。

四、综合应用:ADAS中的实际案例

咱们把上面这些知识点串起来,看一个ADAS中实际会用到的例子——传感器数据融合模块的初始化配置:

// 传感器配置表,使用const保护
typedef struct {
    const char *name;       // 传感器名称,字符串常量
    uint8_t id;             // 传感器ID
    uint16_t data_len;      // 数据长度
    void (*init_func)(void); // 初始化函数指针
} sensor_config_t;

// 定义传感器配置数组(指针数组的变体)
const sensor_config_t sensor_configs[] = {
    {"Camera",    0x01, 640*480, camera_init},
    {"Radar",     0x02, 256,     radar_init},
    {"Lidar",     0x03, 1024,    lidar_init},
    {"Ultrasonic",0x04, 8,       ultrasonic_init}
};

// 初始化所有传感器
void init_all_sensors(void) {
    for (int i = 0; i < sizeof(sensor_configs)/sizeof(sensor_configs[0]); i++) {
        if (sensor_configs[i].init_func) {
            sensor_configs[i].init_func();  // 通过函数指针调用
        }
    }
}

你看,这里用到了:

  • const保护配置表不被修改
  • 结构体包含函数指针,实现多态
  • 数组管理多个传感器配置

这种写法,扩展性极好。新增一个传感器,只需要在数组里加一项,写一个对应的初始化函数。主循环完全不用改。

好了,这一章的内容就这些。指针和数组的高级用法,说白了就是理解「地址」和「内容」的关系,再加上const这把锁,你的代码会安全很多。下一章咱们聊聊内存管理,那又是另一个坑多的地方。