【问题标题】:Auto release of stack variables in C在 C 中自动释放堆栈变量
【发布时间】:2017-12-02 19:36:34
【问题描述】:

不幸的是,在 C 中没有任何智能指针.. 但是是否可以构建一个宏来包装变量声明并在离开声明变量的范围时将该变量作为输入变量调用函数调用?

对不起,这句话很长,但我正在研究 xnu 内核,其中有许多具有内置引用计数器的元素,并且在使用它时一定不要忘记取消引用该元素以避免内存泄漏。

例如,如果我有以下类型的proc_t:

struct proc;
typedef struct proc * proc_t;

我想在一个作用域内声明一个基于该类型的栈变量,例如:

{
    proc_t_release_upon_exit proc_t proc_iter = proc_find(mypid);
    //the rest of the code in this scope 
}

在预处理器分析宏和编译之前,我期望生成以下代码:

{ 
    proc_t myproc = proc_find(mypid)
    //the rest of the code in scope
    proc_rele(myproc);
}

有没有办法像在 C 中那样定义这样的宏?

【问题讨论】:

  • 这是不可能的,因为您希望在两个地方(入口和出口)有代码,而宏只能在一个地方。
  • 使用 C 的工作方法不同。没有必要发明这样的宏。您拥有完全的控制权,因此您可以制作快速的软件——不要猜测它现在在做什么以及为什么慢。
  • 你可以很容易地争论 C 没有智能指针是否是 不幸 - 我认为这是幸运的。
  • C 和 C++ 的主要区别实际上是 C++ 获得了“RAII”(析构函数的自动调用)。大多数其他 C++ 特性(例如封装或多态性)都可以在 C 中实现,但不能在 RAII 中实现。
  • 如果你需要这个你不需要使用C。

标签: c variables macros


【解决方案1】:

您可以在 GCC 中使用 cleanup 变量属性。请看一下: http://echorand.me/site/notes/articles/c_cleanup/cleanup_attribute_c.html

示例代码:

#include <stdio.h>
#include <stdlib.h>

void free_memory(void **ptr)
{
    printf("Free memory: %p\n", *ptr);
    free(*ptr);
}

int main(void)
{
    // Define variable and allocate 1 byte, the memory will be free at
    // the end of the scope by the free_memory function. The free_memory 
    // function will get the pointer to the variable *ptr (double pointer
    // **ptr).
    void *ptr  __attribute__ ((__cleanup__(free_memory))) = malloc(1);
    return 0;
}

如果将源代码保存在名为 main.c 的文件中,则可以使用以下命令对其进行编译:

gcc main.c -o main

并通过以下方式验证是否存在任何内存泄漏:

valgrind ./main

valgrind 的示例输出:

==1026== Memcheck, a memory error detector
==1026== Copyright (C) 2002-2013, and GNU GPL'd, by Julian Seward et al.
==1026== Using Valgrind-3.10.1 and LibVEX; rerun with -h for copyright info
==1026== Command: ./main
==1026== 
Free memory: 0x51ff040
==1026== 
==1026== HEAP SUMMARY:
==1026==     in use at exit: 0 bytes in 0 blocks
==1026==   total heap usage: 1 allocs, 1 frees, 1 bytes allocated
==1026== 
==1026== All heap blocks were freed -- no leaks are possible
==1026== 
==1026== For counts of detected and suppressed errors, rerun with: -v
==1026== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

【讨论】:

  • 如果您可以将自己绑定到专有的、特定于编译器的扩展,我想这是一个可行的解决方案。但是确实有人想知道,为什么不直接将 C 代码编译为 C++ 以从语言中内置的 RAII 中受益呢?这样你就不用依赖任何非便携的东西了,结果会更好。 (我知道,我知道,C++ 不是 C 的 strict 超集,但它非常接近,并且在典型的 C 代码库中几乎不需要修改。)
  • clang 也支持清理属性
  • @CodyGray 并非每个有效的 C 代码在 C++ 中都具有任何语义,即使它可以编译。例如。许多 C 代码使用联合来进行类型双关,但这是 C++ 中的 UB。尽管 GCC 确实以与 C 中相同的方式定义它,但在这种情况下切换到 C++ 并不会使您的代码更具可移植性。
  • @immibis "proprietary" 暗示在其他地方重新实现同等行为在法律上和字面上是不可能的;人们可以并且确实为 clang 编写扩展,允许 GNU C 在其中编译
  • “专有”有多种定义;特别是这个:“非标准且仅由一个特定组织使用,如对标准的专有扩展”。我没有创新这种用法;对于CSS extensions,“专有”也以这种方式使用,尽管 Mozilla 就像 GCC 一样是开源的。关键是这种行为不是 C 语言标准的一部分,因此是不可移植的,其效果是将你锁定在实际 意义上的GCC 中,而不是合法的一。 @猫
【解决方案2】:

C 确实提供了一种在语法上将代码放置在首先执行的其他代码之前的方法:for 块。请记住,for 结构的第 3 条可以包含任意表达式,并且始终在主块执行之后运行。

因此,您可以通过将for 块包装在宏中来创建一个宏,在给定的以下代码块之后进行预定调用:

#define M_GEN_DONE_FLAG() _done_ ## __LINE__ 

#define M_AROUND_BLOCK2(FLAG, DECL, BEFORE, AFTER) \
  for (int FLAG = (BEFORE, 0); !FLAG; ) \
    for (DECL; !FLAG; FLAG = (AFTER, 1))

#define M_AROUND_BLOCK(DECL, BEFORE, AFTER) M_AROUND_BLOCK2(M_GEN_DONE_FLAG(), DECL, BEFORE, AFTER)

#define M_CLEANUP_VAR(DECL, CLEANUP_CALL) M_AROUND_BLOCK(DECL, (void)0, CLEANUP_CALL)

...你可以这样使用它:

#include <stdio.h>

struct proc;
typedef struct proc * proc_t;

proc_t proc_find(int);
void proc_rele(proc_t);

void fun(int mypid) {
  M_CLEANUP_VAR (proc_t myproc = proc_find(mypid), proc_rele(myproc))
  {
    printf("%p\n", &myproc); // just to prove it's in scope
  }
}

这里的诀窍是 for 块接受以下语句,但如果我们实际上没有将该语句放入宏定义中,我们可以使用普通代码块跟随宏调用,它会“神奇地”属于我们新的作用域控制结构语法,只需遵循扩展的for。

任何值得使用的优化器都会在其最低优化设置下删除循环标志。请注意,名称与标志冲突并不是一个大问题(即,您实际上并不需要gensym),因为标志的范围是循环体,如果使用相同的任何嵌套循环,它们都会安全地隐藏它标志名称。

这里的好处是要清理的变量的范围受到限制(它不能在其声明之后立即在化合物之外使用)并且在视觉上是明确的(因为所述化合物)。

优点:

  • 这是没有扩展的标准 C
  • 控制流程很简单
  • 实际上(不知何故)没有__attribute__ __cleanup__那么冗长

缺点:

  • 它不提供“完整”的 RAII(即不能防止 goto 或 C++ 异常:__cleanup__ 通常在底层使用 C++ 机制实现,因此它更完整)。更严重的是,它不能防止早期的return(感谢@Voo)。 (您可以至少防止出现错误的break - 如果您愿意 - 通过在M_AROUND_BLOCK2 的末尾添加第三行switch (0) default:。)
  • 并非所有人都同意语法扩展宏(但考虑到您正在在此处扩展 C 的语义,所以...)

【讨论】:

  • 哇。与大多数这样的事情一样,我不知道是惊讶还是恐惧,或者两者兼而有之。预处理器运行后,您的示例是什么样的?
  • 除了不能防止诸如 goto 或 C++ 异常之类的模糊事物之外,它也不能防止简单的return 这似乎更容易引起麻烦。
  • 我想知道是否可以通过一个 for 循环来完成,类似于:for(init_code, flag=1; flag--; cleanup){/*your code here*/}
  • @Voo 这是我的偏好是拆分内存管理和功能组件。被调用的函数设置内存,然后调用函数组件,当 that 返回(无论出于何种原因)时,清理会自动完成。
  • 如果你能写something() { ... }那你为什么不能写something() ... something_else()?我看不出有什么区别。如果允许在函数末尾键入内容,则不妨键入foo() 和},这样也可以解决整个问题。
【解决方案3】:

我知道这不是你想听到的,但我敦促你不要这样做。

单点返回是完全可以接受的 C 风格,在此之前一切都会被清理干净。由于没有异常,这很容易做到,并且通过查看函数很容易验证。

使用宏黑客或编译器“功能”来做到这一点是不被接受的 C 风格。阅读理解之后,会成为大家的负担。最后,它实际上并没有给你带来太多好处。

【讨论】:

  • 您究竟是希望提问者不要做什么? __attribute__ ((__cleanup__(free_memory))) 的回答不会受到您的反对。
  • @PJTraill __attribute__ 的答案大多无关紧要,因为它不是标准的,而且 OP 从未提到 gcc。现在 OP 接受了它,所以我认为它为他们解决了问题,但这对未来使用其他编译器的读者没有帮助。
  • 总体而言,对此类问题有一个“要求理智”的答案总是很好,即使他们没有回答问题。将防止其他人将他们的程序脱轨到一些宏观地狱。
  • @PJTraill 我的反对意见是它不标准且非常不寻常,让人难以理解。
  • 我同意“最小意外原则”,但由于代码三明治是非常常见的错误来源,我倾向于尖叫“用 RAII 扩展 C!或者采用更好的习语!”,并高兴地否决你的答案。
猜你喜欢
  • 2021-12-18
  • 2012-02-20
  • 2018-06-01
  • 2013-11-13
  • 2011-04-13
  • 1970-01-01
  • 2021-09-27
  • 2019-04-15
  • 2018-11-29
相关资源
最近更新 更多