PAYATHON 2026

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 字段,结构体能保留类型信息,避免写错下标顺序,通常更适合这个程序。

备注:内容仅供参考。