如何在C语言中取消声明(删除)变量?

32

就像我们处理宏一样:

#undef SOMEMACRO 

我们能否在C语言中取消声明删除变量,以便节省大量的内存?

我知道malloc()free(),但我希望完全删除变量,这样如果我使用printf("%d", a);就应该会出错。

test.c:4:14: error: ‘a’ undeclared (first use in this function)

19
在变量周围使用紧密的范围(也就是一对大括号包含一系列语句)。当定义变量的范围退出时,变量将被销毁(并且只有在进入范围时才会创建)。否则,答案就是否定的。全局变量根本无法被销毁。 - Jonathan Leffler
任何局部变量在离开其作用域时都会停止存在。在全局作用域中,除了程序终止外,没有其他方法。 - 0___________
相关:https://dev59.com/gnE85IYBdhLWcg3wejZO - alk
5个回答

58
不可以,但你可以创建小的最小作用域来实现这一点,因为所有作用域局部变量都会在退出作用域时被销毁。类似这样的代码:
void foo() {
    // some codes
    // ...
    {    // create an extra minimum scope where a is needed
        int a;
    }
    // a doesn't exist here
}

9
事实上,如果您通过这种方式定义不重叠作用域的变量,编译器可以重新使用堆栈变量空间。 即使您没有这样做,编译器也可以进行这样的优化。 - immortal
18
你曾经做过嵌入式系统吗?我曾经使用一个只有128字节堆栈的控制器和一个编译器,这个编译器非常严重地让函数之间的堆栈变量重叠。由于函数指针使用导致编译器无法正确计算重叠的堆栈帧,这导致了运行时错误。那可真是美好的时光 :) - immortal
12
@MikeNakis 曾说过:“编译器从来不会重复使用堆栈空间,也不要这样做,很可能以后也不会这样做。” 但是我的 GCC 编译器会像这里描述的那样重复使用堆栈空间:https://dev59.com/gnE85IYBdhLWcg3wejZO#2759834 我刚刚测试了这段代码:void foo(void) { { char a[1024 * 1024 * 4] = {0}; } { char b[1024 * 1024 * 4] = {0}; } } 可以正常工作,而 void foo(void) { char a[1024 * 1024 * 4] = {0}; char b[1024 * 1024 * 4] = {0}; } 就行不通。 - alk
10
对于作用域不重叠的变量,重复使用栈空间并不需要调整栈指针。这本质上将该部分栈帧视为“联合体”。 - Barmar
12
编译器不需要有限范围以重复使用内存寄存器。它只需使用流程图来推理非重叠生命周期。甚至可以在不同时间将同一变量放置在不同的位置/寄存器中(至少当地址未被获取时)。由于编译器通常在优化过程中使用SSA形式,因此将存储位置和变量解耦非常自然。 - CodesInChaos
显示剩余12条评论

27

这并非对问题的直接回答,但可以澄清为什么该问题没有适当的答案以及为什么在C语言中“删除”变量是不可能的。

第一点:什么是变量?

变量是程序员给一个内存空间分配一个名称的方式。这很重要,因为这意味着变量不必占用任何实际空间!只要编译器有一种跟踪相关内存的方法,定义的变量可以被转换成许多方式之一来不占用任何空间。 例如:const int i = 10; 编译器可以轻松地选择将所有 i 的实例替换为立即值。在这种情况下,i 将不占用任何数据存储器(取决于架构,它可能会增加代码大小)。或者,编译器可以将值存储在寄存器中,此时也不会使用堆栈或堆空间。在运行时不存在的标签“未定义”是没有意义的。

第二点:变量存储在哪里?

在第一点之后,您已经了解到这不是一个容易回答的问题,因为编译器可以自由地进行任何操作而不会破坏您的逻辑,但一般来说,变量存储在堆栈上。堆栈的工作原理对于您的问题非常重要。 当调用函数时,机器将CPU指令指针的当前位置和当前堆栈指针推入堆栈中,将堆栈指针替换为堆栈上的下一个位置。然后跳转到被调用的函数代码。

这个函数知道它有多少个变量以及它们需要多少空间,因此它移动框架指针以捕获可以占用所有函数变量的帧,然后只使用堆栈。为了简化事情,该函数从一开始就捕获足够的空间来容纳其所有变量,并且每个变量都有从函数堆栈帧*开始的明确定义的偏移量。变量也是相继存储的。

虽然在执行操作后您可以操纵帧指针,但这样做将非常昂贵且大多数情况下是无意义的 - 运行代码仅使用最后一个堆栈帧,并且如果需要可以占用所有剩余的堆栈(堆栈在线程启动时分配),因此“释放”变量几乎没有什么好处。从堆栈帧中间释放变量需要进行碎片整理操作,这将非常耗费CPU并且恢复少量内存是没有意义的。

要点#3:让编译器完成其工作

这里的最后一个问题是简单的事实:编译器可以比您更好地优化程序。在需要时,编译器可以检测变量范围和重叠的内存,以减少程序的内存消耗(-O3编译标志)。您不需要“释放”变量,因为编译器可以在您不知道的情况下完成。

这是为了补充之前所说的关于变量太小而无法实现您所要求的机制的所有内容。


* 支持动态大小数组的语言可以修改堆栈帧,在计算出数组大小后仅为该数组分配空间。


3
第三点尤其相关,编译器通常会像寄存器一样定期回收堆栈空间以供其他变量使用;这是在进行逆向工程时必须注意的事情——即使你已经了解一个给定位置在堆栈上引用哪个局部变量,一旦被覆盖,它也可能成为完全不同的变量。 - Matteo Italia
1
这应该是这里的第一答案。 - Display Name
许多编译器,特别是用于嵌入式应用的编译器(例如XC8),除非您付费购买高级版本,否则不允许使用更高级别的优化。话虽如此,“让编译器完成其工作”的观点仍然可能是最好的答案。 - Charlie

17

无论是在 C 还是在绝大多数编程语言中,都没有办法做到这一点,至少在我所了解的所有编程语言中都是如此。

而且你也不会节省很多内存。如果你这样做,你将节省的内存量微不足道。非常小。不值得谈论。

用这种方式清理变量的机制可能占用的内存比你要清除的变量还要多。

回收单个变量的代码调用本身所占空间也会比变量本身更大。

因此,即使有一个神奇的方法 purge() 可以清除变量,purge() 的实现大小也比你希望通过清除程序变量来恢复的任何内存量更大,而且在 int a; purge(a); 中,对 purge() 的调用所占空间也比 a 本身还要大。

这是因为你所说的变量非常小。你提供的 printf("%d", a); 示例表明你在考虑以某种方式回收单个 int 变量所占用的内存。即使有一种方法可以做到这一点,你也只能节省大约 4 字节左右。这些变量所占用的总内存非常小,因为它直接取决于你作为程序员手动输入声明变量的数量。在键盘上无意识地声明变量数年之后,你才可能声明出占用值得一提的内存量的 int 变量。


有 R 这种编程语言,可以删除变量包括名称。虽然它是一种脚本语言。 - stefan
3
还可以使用Python、Matlab等语言来完成,否则答案就会完全偏离正确范畴。 - ojs

6

你可以使用代码块({ })和尽可能晚地定义变量来限制它存在的范围。

但是,除非变量的地址被取出,否则这样做对生成的代码没有任何影响,因为编译器确定变量值所需保留的作用域并不会受到重大影响。

如果变量的地址被取出,则逃逸分析失败,这主要是由于内联屏障(如独立编译或允许语义插入)而导致的,这会使编译器认为必须将其保持活动状态直到块结束。这很少有影响(不用担心一些 int 变量,通常只需要多几行代码就能保持其活动状态),但最好记住,在极少数情况下可能会有影响。


2
如果你很关注栈上的很少内存,那么你可能会对你的编译器有具体的了解。你需要找出它在编译时做了什么。C语言并没有明确指定栈帧的实际形状,这留给编译器去解决。以目前接受的答案为例:
void foo() {
    // some codes
    // ...
    {    // create an extra minimum scope where a is needed
        int a;
    }
    // a doesn't exist here
}

这可能会影响函数的内存使用情况,也可能不会。如果您在像gcc或Visual Studio这样的主流编译器中执行此操作,您会发现它们优化速度而不是堆栈大小,因此它们会在函数开始时预先分配所有所需的堆栈空间。它们将进行分析以确定所需的最小预分配量,使用您的作用域和变量使用分析,但这些算法不会受到额外作用域的影响。它们已经比那聪明了。
其他编译器,特别是嵌入式平台的编译器,可能会以不同的方式分配堆栈帧。在这些平台上,这种作用域可能是您需要的技巧。如何区分?唯一的选择是:
- 阅读文档 - 尝试并查看哪个有效
还要确保您了解问题的确切性质。我曾在一个特定的嵌入式项目上工作,除了返回值和一些int之外,它避免使用栈来处理“所有”东西。当我向高级开发人员询问这种愚蠢的做法时,他们解释说,在这个特定的应用程序中,堆栈空间比全局分配的变量空间更为紧缺。他们有一个过程,必须证明系统将按预期运行,如果他们预先分配所有内容并避免递归,这个过程对他们来说会容易得多。我保证,除非您首先知道解决问题的确切性质,否则您永远不会想出这样一个错综复杂的解决方案。
作为另一种解决方案,您可以查看自己构建堆栈帧的方法。制作一个结构体的联合体,其中每个结构体包含一个堆栈帧的变量。然后自己跟踪它们。您还可以查看像alloca这样的函数,如果您的编译器支持,则可以在函数调用期间允许增加堆栈帧大小。
联合体结构体是否有效?尝试一下。答案取决于编译器。如果所有变量都存储在特定设备的内存中,则此方法可能会最小化堆栈使用情况。但是,它也可能会大大混淆寄存器着色算法,并导致堆栈使用增加!尝试并查看它对您的影响!

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接