SDL3 如何通过 appstate 传递包含多个指针的数组?
可以通过 appstate 传递包含多个指针的状态对象。原代码有两个问题:给 appstate 赋值只会修改函数内的参数副本,而局部数组 state 的生命周期在 SDL_AppInit 返回时结束。需要通过 *appstate 写回状态对象的指针,并确保该对象在后续回调使用期间保持有效。
对于 SDL_Renderer* 和 SDL_Window* 这样的不同类型,用结构体保存更清楚,也能减少类型转换错误。
为什么原来的赋值有效,修改后却失效?
SDL_AppInit 的参数是:
void **appstate
这里的 void** 用于让初始化函数写回一个 void* 状态值。它不表示 SDL 提供了指针数组,也不包含数组长度信息。
原来的代码:
*appstate = renderer;
这行代码把 renderer 指针保存到调用方提供的状态位置中。后续 SDL_AppIterate 接收到这个值后,可以将其转换回 SDL_Renderer*。
修改后的代码:
appstate = (void **)&state;
这只改变了局部参数 appstate 的指向,没有修改调用方保存的状态值。强制类型转换也无法改变这一点。
另外,下面的声明创建了一个包含两个 void* 元素的数组:
void *state[2];
它不是“指向长度为 2 的数组的指针”。在通常的表达式中,state 可以退化为指向首元素的 void**;&state 的类型则是 void* (*)[2],即指向整个数组的指针。
即便将赋值改为:
*appstate = state;
问题仍然存在。state 是自动存储期的局部数组,SDL_AppInit 返回后就不再有效,后续通过该地址读取元素属于未定义行为。window 和 renderer 所指资源的生命周期,与存放这些指针的数组的生命周期是分开的。
推荐做法:使用结构体保存状态
在文件作用域定义状态类型,并添加 ,供下面的 std::nothrow 分配使用:
#include <new>
struct AppState {
SDL_Renderer *renderer = nullptr;
SDL_Window *window = nullptr;
};
保留原程序的头文件、常量和事件回调,将 SDL_AppInit 替换为:
SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[]) {
*appstate = nullptr;
auto *state = new (std::nothrow) AppState{};
if (!state) {
SDL_Log("Couldn't allocate application state");
return SDL_APP_FAILURE;
}
if (!SDL_CreateWindowAndRenderer(
MESSAGE, WIDTH, HEIGHT, SDL_WINDOW_RESIZABLE,
&state->window, &state->renderer)) {
SDL_Log("Couldn't create window and renderer: %s", SDL_GetError());
if (state->renderer) {
SDL_DestroyRenderer(state->renderer);
}
if (state->window) {
SDL_DestroyWindow(state->window);
}
delete state;
return SDL_APP_FAILURE;
}
*appstate = state;
return SDL_APP_CONTINUE;
}
动态分配的 AppState 不会在 SDL_AppInit 返回时销毁。*appstate = state 保存它的地址,供后续回调使用。
在 SDL_AppIterate 开头,将原来的类型转换替换为:
auto *state = static_cast<AppState *>(appstate);
auto *local_renderer = state->renderer;
auto *local_window = state->window;
后面的绘制代码可以继续使用 local_renderer。如果暂时用不到窗口指针,可以不声明 local_window,需要时直接读取 state->window。
退出时,在 SDL_AppQuit 中销毁资源并释放状态对象:
void SDL_AppQuit(void *appstate, SDL_AppResult result) {
auto *state = static_cast<AppState *>(appstate);
if (!state) {
return;
}
if (state->renderer) {
SDL_DestroyRenderer(state->renderer);
}
if (state->window) {
SDL_DestroyWindow(state->window);
}
delete state;
}
这里先销毁 renderer,再销毁 window,最后释放保存这些指针的 AppState。初始化失败路径已经自行清理,没有把已释放的地址写入 *appstate。
如果确实想使用指针数组
也可以使用指针数组,但要处理好存储期和释放问题。以下是另一套实现方式,不要与上面的结构体版本混用。
在 SDL_AppInit 开头设置:
*appstate = nullptr;
保留原有的窗口和渲染器创建代码,创建成功后用动态数组保存指针:
auto **state = new (std::nothrow) void*[2]{renderer, window};
if (!state) {
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
return SDL_APP_FAILURE;
}
*appstate = state;
return SDL_APP_CONTINUE;
同样需要包含 。动态数组在显式释放前会一直存在,后续回调可以按约定的下标取回指针:
auto **state = static_cast<void **>(appstate);
auto *local_renderer = static_cast<SDL_Renderer *>(state[0]);
auto *local_window = static_cast<SDL_Window *>(state[1]);
退出时,对应的清理代码为:
void SDL_AppQuit(void *appstate, SDL_AppResult result) {
auto **state = static_cast<void **>(appstate);
if (!state) {
return;
}
SDL_DestroyRenderer(static_cast<SDL_Renderer *>(state[0]));
SDL_DestroyWindow(static_cast<SDL_Window *>(state[1]));
delete[] state;
}
这段清理代码对应“窗口和渲染器创建成功后才保存数组”的路径。初始化过程中的其他失败分支,也应清理各自已经取得的资源。
使用时要注意什么?
- 在 SDL_AppInit 中通过 *appstate 写回状态指针,后续回调直接使用收到的 appstate。
- 指针转换不会复制数组、延长对象生命周期,也不会自动管理 SDL 资源。
- 保存什么类型,就按对应类型取回。改为保存 AppState* 后,不能继续将 appstate 直接转换为 SDL_Renderer*。
- new 创建的状态对象使用 delete;new[] 创建的数组使用 delete[]。二者都不会自动销毁数组或结构体内指针所指向的 SDL 资源。
- 对于固定的 renderer 和 window 字段,结构体能保留类型信息,避免写错下标顺序,通常更适合这个程序。
备注:内容仅供参考。
