ESP32 中 C++ 对象的引脚成员为何在运行时变成异常值?
结论:NavigationManager 把单个对象当成了对象数组
复现代码中,prox_sensors 是一个包含五个对象指针的数组,但传给 NavigationManager 的是第一个对象的指针 prox_sensors[0]。管理器随后通过 proxSensors[i] 访问它,相当于把这个指针当成了连续对象数组的起点。
五次独立的 new ProximitySensor(...) 不会创建一个包含五个元素的对象数组。当 i >= 1 时,这种访问会产生未定义行为,可以解释引脚编号和安装方向的异常。应让管理器接收整个指针数组及其长度,再通过数组中的指针访问对象。
为什么第一个传感器正常,后面的值却像随机数?
原始代码创建的是指针数组:
ProximitySensor *prox_sensors[5] = {
new ProximitySensor(23, 36, 0),
new ProximitySensor(22, 39, -25),
new ProximitySensor(19, 35, 25),
new ProximitySensor(18, 32, -120),
new ProximitySensor(21, 34, 120)
};
连续存放的是五个指针,每个指针都指向一个独立创建的对象。这些对象之间不保证连续,不能通过对象指针的下标依次访问。
问题出在下面两处代码的组合:
NavigationManager driveManager(prox_sensors[0]);
ProximitySensor sensor = proxSensors[i];
prox_sensors[0] 的类型是 ProximitySensor*,指向第一个传感器对象。表达式 proxSensors[i] 等价于:
*(proxSensors + i)
它按 sizeof(ProximitySensor) 的步长寻找第 i 个对象,不会从原来的指针数组中取下一个指针。
- 当
i == 0时,访问的是第一个真实对象,因此日志中出现了正确的0、23和36。 - 当
i >= 1时,代码访问的不是通过该指针合法索引得到的传感器对象。将那里的内容复制为ProximitySensor,可能得到负数引脚或异常方向,也可能触发崩溃。
日志中的 GPIO output gpio_num error 与异常引脚值一致:read() 将这些无效编号传给了 digitalWrite()。仅凭日志无法判断每个异常值来自哪一块内存,但复现代码中的非法访问可以确定。
修改方法:传入指针数组和实际数量
需要同时修改成员类型、构造函数、构造调用和读取方式。只把调用改成 driveManager(prox_sensors),原来的构造函数仍无法正确接收它。
下面的代码可以替换复现代码中的 NavigationManager.h。传感器数量由调用方提供,循环通过 *proxSensors[i] 取得真实对象的引用:
#ifndef DRIVERMANAGER_H
#define DRIVERMANAGER_H
#include <Arduino.h>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include "../sensors/impl/ProximitySensor.h"
#define OBS_GRID_SIZE 50
#define OBS_CELL_SIZE 20
class NavigationManager
{
private:
uint8_t obstacleGrid[OBS_GRID_SIZE][OBS_GRID_SIZE]{};
int x;
int y;
float heading;
ProximitySensor *const *proxSensors;
std::size_t sensorCount;
public:
NavigationManager(ProximitySensor *const *sensors,
std::size_t count)
: x(0),
y(0),
heading(0),
proxSensors(sensors),
sensorCount(count)
{
}
void updateMapFromSensors(bool isExploring_p)
{
for (std::size_t i = 0; i < sensorCount; ++i)
{
ProximitySensor &sensor = *proxSensors[i];
float reading = sensor.read();
uint8_t maxDistance = isExploring_p ? 200 : 50;
if (reading > maxDistance || reading < 5)
continue;
int16_t absolute_orientation =
static_cast<int>(heading + sensor.getOrientation()) % 360;
double rad_angle = absolute_orientation * PI / 180.0;
double dx_grid =
(reading * std::cos(rad_angle)) / OBS_CELL_SIZE;
double dy_grid =
(reading * std::sin(rad_angle)) / OBS_CELL_SIZE;
int gridX = static_cast<int>(x + dx_grid);
int gridY = static_cast<int>(y + dy_grid);
if (gridX >= 0 && gridX < OBS_GRID_SIZE &&
gridY >= 0 && gridY < OBS_GRID_SIZE)
{
obstacleGrid[gridX][gridY] = 1;
}
}
}
};
#endif
ProximitySensor *const * 表示管理器通过这个成员读取指针数组时,不能改写数组里的指针,但仍可以调用传感器对象的非 const 方法,例如 read()。
在 main.cpp 中保留原来的 prox_sensors 声明,并将管理器的构造代码改为:
constexpr std::size_t proxSensorCount =
sizeof(prox_sensors) / sizeof(prox_sensors[0]);
NavigationManager driveManager(prox_sensors, proxSensorCount);
初始化传感器时也使用同一个实际数量:
void setupProxSensors()
{
for (std::size_t i = 0; i < proxSensorCount; ++i)
{
prox_sensors[i]->setup();
}
}
这里的 sizeof 必须用在仍然是数组的 prox_sensors 上。传入函数后,单凭指针无法用这个公式恢复数组长度。
上述实现假设每个数组元素都指向有效的传感器对象,并且指针数组和对象在管理器使用期间始终存在。复现代码中的数组位于全局作用域,对象也没有被释放,符合这一生命周期要求。管理器本身不负责释放这些对象。
const 为什么没有阻止异常值?
const 限制通过正常的 C++ 操作修改成员。例如,下面的赋值会被编译器拒绝:
pin_trig = 99;
它不提供运行时内存保护,也不能让越界访问变得有效。当前代码甚至不一定修改了原始对象:它可能只是把其他内存中的内容当成对象读取,因此看起来像是成员发生了变化。
原代码中的对象复制本身也不是根因:
ProximitySensor sensor = proxSensors[i];
从有效对象进行复制初始化时,可以复制其 const 成员。这里的下标访问没有取得有效对象。改用引用可以避免不必要的复制,但必须先让指针数组的类型和访问方式对应起来。
修改后应检查什么?
先核对每轮读取的方向与引脚是否仍对应初始化参数:
| orientation | pin_trig | pin_echo |
|---|---|---|
| 0 | 23 | 36 |
| -25 | 22 | 39 |
| 25 | 19 | 35 |
| -120 | 18 | 32 |
| 120 | 21 | 34 |
这些是预期值,仍需在开发板上验证。现有材料足以确认上述越界访问,但不能据此排除完整项目中的其他内存错误。如果修改后仍有异常,再检查其他数组访问、对象生命周期和写入位置。
剩余堆内存超过 200KB、最大可分配块约为 110KB,只能反映当时的可用堆情况,不能证明所有指针访问合法。越界读取不需要耗尽内存,也不一定改变这两个指标。
复现代码中的单位注释也需要修正:
#define SOUND_SPEED 0.0343 // cm/microsecond
以 0.0343 作为声速系数、duration 使用微秒时,duration * SOUND_SPEED / 2 得到的距离单位是厘米,因此应确认 OBS_CELL_SIZE 和距离阈值也使用厘米。单位注释与对象成员异常是两个独立问题。
备注:内容仅供参考。
