13. 资源的生命周期管理 + {} 限制局部作用域

你观察得非常仔细,这个 {} 构成的局部作用域(Local Scope)在这里起着一个至关重要的作用,它与 C++ 的一个核心特性——RAII (Resource Acquisition Is Initialization) 紧密相关,并且确实和你提到的栈分配 (Stack Allocation) 有关。

让我们来详细解释这个作用域的目的。


问题的核心:资源的生命周期管理

在你的代码中,你创建了一些封装了 OpenGL 对象的 C++ 类,比如:

  • VertexBuffer vb(...)
  • IndexBuffer ib(...)

这些类很可能是按照 RAII 原则设计的(这是 Cherno 教程中的一个关键点)。

RAII 的核心思想是

  • 在对象的构造函数中获取资源(比如调用 glGenBuffers 创建一个 VBO)。
  • 在对象的析构函数中释放资源(比如调用 glDeleteBuffers 删除这个 VBO)。

当一个对象是在栈 (Stack) 上创建时,它的生命周期就和它所在的作用域绑定了。一旦程序执行离开这个作用域(无论是正常结束还是因为异常跳出),这个对象就会被自动销毁,它的析构函数 (destructor) 就会被自动调用

分析你的代码中的作用域

我们来看看这个作用域内发生了什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// ... glewInit() ...

/* Because the */ // (这里的注释可能是 "Because the destructors need to be called")
{
// ---- 进入作用域 ----

// 1. 创建 VAO (这是一个原始的 unsigned int,不是一个类对象)
unsigned int vao;
GLCall(glGenVertexArrays(1, &vao));
// ...

// 2. 在栈上创建 VertexBuffer 对象
// - `VertexBuffer` 的构造函数被调用,内部执行 glGenBuffers, glBindBuffer, glBufferData
VertexBuffer vb(&pos, 8 * sizeof(float));

// 3. 在栈上创建 IndexBuffer 对象
// - `IndexBuffer` 的构造函数被调用,内部执行 glGenBuffers, ...
IndexBuffer ib(&indices, 6);

// 4. 创建 Shader Program (这是一个原始的 unsigned int)
unsigned int program = CreateShaderProgram(...);

// 5. 进入主循环
while (!glfwWindowShouldClose(window))
{
// ... 使用 vb, ib, program, vao进行渲染 ...
}

// 6. 手动删除 Shader Program
GLCall(glDeleteProgram(program));

} // <--- 关键点在这里:离开作用域

// 7. 调用 glfwTerminate()
glfwTerminate();

当程序执行到 } 时会发生什么?

  1. 栈对象逆序销毁:程序执行流离开了这个局部作用域。所有在这个作用域的栈上创建的对象,会以它们创建时相反的顺序被自动销 chiffres。

    • ib (IndexBuffer 对象) 首先被销毁,它的析构函数 ~IndexBuffer() 被自动调用。在这个析构函数内部,很可能执行了 glDeleteBuffers(1, &m_RendererID) 来释放 IBO。
    • vb (VertexBuffer 对象) 接着被销毁,它的析构函数 ~VertexBuffer() 被自动调用。在这个析构函数内部,执行了 glDeleteBuffers(1, &m_RendererID) 来释放 VBO。
  2. vaoprogram 不受影响:因为 vaoprogram 只是普通的 unsigned int,它们不是类对象,没有析构函数。它们的值只是从栈上被弹出,但它们所代表的 OpenGL 资源不会被自动释放。这就是为什么你需要手动调用 glDeleteProgram(program)。你也应该在这里加上 glDeleteVertexArrays(1, &vao)

这个作用域的核心作用

确保在 OpenGL 上下文 (Context) 被销毁之前,所有与该上下文相关的 GPU 资源都已经被正确释放。

让我们看看如果没有这个作用域会发生什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ... glewInit() ...

VertexBuffer vb(...);
IndexBuffer ib(...);

// ... 渲染循环 ...

// 离开 main 函数作用域
glfwTerminate(); // <--- OpenGL 上下文在这里被销毁了!
return 0;

// 在 glfwTerminate 之后,vb 和 ib 的析构函数才会被调用
// 但是此时 OpenGL 上下文已经不存在了!
// 调用 glDeleteBuffers 将会失败,或者导致未定义行为。
// 这就是所谓的“资源泄露”,因为 GPU 资源没有被正确释放。

所以,这个 {...} 的作用就是:

  1. 创建一个受控的生命周期:它定义了一个明确的区域,所有基于 RAII 的 OpenGL 封装类的生命周期都限制在这个区域内。
  2. 保证正确的销毁顺序:确保在 glfwTerminate() 被调用(销毁 OpenGL 上下文)之前,所有封装类的析构函数都已经被自动调用,从而安全地释放了 VBO, IBO 等 GPU 资源。

总结

这个局部作用域是利用 C++ RAII 特性来自动化和安全地管理 OpenGL 资源的一种非常优雅和强大的技术。

  • 它与栈分配 (Stack Allocation) 相关:因为只有在栈上创建的对象才能在离开作用域时自动调用析构函数。
  • 它解决了资源管理的核心痛点:避免了手动调用 glDeleteBuffers 等函数,减少了因忘记释放资源而导致的内存泄漏,并保证了资源在正确的时间点被释放。

这就是现代 C++ 与 C-style API (如 OpenGL) 相结合的最佳实践之一。

提问内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
int main(void) {
    GLFWwindow* window;
    if (!glfwInit())
        return -1;
    window = glfwCreateWindow(640, 480, "Hello World", NULL, NULL);
   
    glfwMakeContextCurrent(window);
    glfwSwapInterval(1);

    if (glewInit() != GLEW_OK)
        std::cerr << " Error init glew." << std::endl;

    {
        float pos[] = {
            -0.5f, -0.5f,
            +0.5f, -0.5f,
            +0.5f, +0.5f,
            -0.5f, +0.5f
        };

        unsigned int indices[] = {
            0, 1, 2,
            2, 3, 0
        };

        unsigned int vao;

        GLCall(glGenVertexArrays(1, &vao));
        GLCall(glBindVertexArray(vao));
// Create vertex buffer here
        VertexBuffer vb(&pos, 8 * sizeof(float));

        unsigned int AttribIndex = 0;
        GLCall(glEnableVertexAttribArray(AttribIndex));
        GLCall(glVertexAttribPointer(AttribIndex, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2, (const void*)0));

// Create index buffer here
        IndexBuffer ib(&indices, 6);

        ShaderProgramSource ShaderSource = ParseShader("res/shaders/Basic.shader");

        unsigned int program = CreateShaderProgram(ShaderSource.VertexSource, ShaderSource.FragmentSource);

        GLCall(glUseProgram(program));
        GLCall(int location = glGetUniformLocation(program, "u_Color"));

        ASSERT(location != -1);

        float r = 0.0f;

        float increment = 0.01f;

        while (!glfwWindowShouldClose(window)) {

            GLCall(glClear(GL_COLOR_BUFFER_BIT));
            GLCall(glUniform4f(location, r, 0.3f, 0.8f, 1.0f));

            if (r > 1.0f || r < 0.0f)
                increment = -increment;

            r += increment;
            GLCall(glBindBuffer(GL_ARRAY_BUFFER, 0));
            GLCall(glBindVertexArray(vao));
            GLCall(glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr));
            GLCall(glfwSwapBuffers(window));
            GLCall(glfwPollEvents());
        }

        GLCall(glDeleteProgram(program));
    }
    glfwTerminate();
    return 0;
}

解释一下在这里的glfwTerminate之前的}和注释/* Because the */后面的{,这个局部作用域有什么作用。提示,这里好像有关于 stack alloac