【问题标题】:Inconveniences of pointers to static variables指向静态变量的指针的不便之处
【发布时间】:2011-01-14 22:28:45
【问题描述】:

我经常使用像这样返回指向静态缓冲区的指针的便捷函数:

char* p(int x) {
    static char res[512];

    snprintf(res, sizeof(res)-1, "number is %d", x));

    return res;
}

并将它们用作其他函数的参数:

...
some_func( somearg, p(6) );
....

但是,这种“方便”除了不是线程安全的(可能还有更多原因)之外还有一个烦人的缺点:

some_func( somearg, p(6), p(7) );

上面显然没有做我想要的,因为最后两个参数将指向相同的内存空间。我希望能够在没有太多麻烦的情况下使上述内容正常工作。

所以我的问题是:

是否有一些我错过的神奇方法可以在不进行繁琐的分配和释放的情况下完成我想要的?

***** 2010-04-20 更新 *****

无耻插件:看我自己的回答here

我想它会起作用,但它也有点矫枉过正。意见?

【问题讨论】:

  • 在 C 领域内,并没有什么好的答案。在某些情况下,您可以通过要求调用者传递缓冲区来解决问题,但这会失去很多便利。
  • 这不仅仅是线程不安全的,你会得到意想不到的副作用。假设您连续两次调用该函数,将结果分配给两个不同的指针。当你输出两个字符串时,你会得到第二个值两次,因为两个指针都指向同一个缓冲区。

标签: c pointers memory-management


【解决方案1】:

让调用者提供缓冲区(以及缓冲区的大小)。它是线程安全的,缓冲区通常可以放在堆栈上,因此不会带来堆分配的开销。

【讨论】:

    【解决方案2】:

    嗯,一种广泛使用的方法是将准备内存缓冲区的责任交给调用者。调用者可以选择它最喜欢的任何方法。

    在您的情况下,请将您的 p 写为

    char* p(char *buffer, size_t max_length, int x) { 
      snprintf(buffer, max_length, "number is %d", x); 
      return buffer; 
    } 
    

    并将其称为

    char buffer1[512], buffer2[512];   
    some_func( somearg, p(buffer1, sizeof buffer1 - 1, 6), p(buffer2, sizeof buffer2 - 1, 7) );   
    

    这种方法至少有一个明显的缺点:在一般情况下,调用者事先并不知道它需要为缓冲区分配多少字符。如果有一个好的常量编译时值可用,那么这很容易,但在更复杂的情况下,需要额外的努力,比如提供某种“预计算”功能,将所需的缓冲区大小作为运行时值返回. (snprintf 函数实际上是这样工作的:您可以使用空缓冲区指针和零缓冲区大小调用它,只是为了确定缓冲区大小进行虚构运行。

    【讨论】:

      【解决方案3】:

      简而言之,没有。 C 不提供任何形式的自动堆管理,因此您可以自己跟踪分配的内存。一个标准的类 C 解决方案是让调用者提供一个缓冲区,而不是在内部分配一个。尽管这只是转移了跟踪内存的责任,但它通常最终会出现在更方便的地方。如果您想在 C 中获得某种形式的垃圾收集,我想您可以查看 Boehm's conservative garbage collector

      【讨论】:

        【解决方案4】:

        只要您理解这是线程不安全的,并且您的逻辑期望“便利”方法返回的值仅在 少数 次调用 [可能有多种] 方法,您可以通过两种不相关且可能互补的方式来扩展这种便利性。

          1234563只要调用者提供缓冲区,就使用它,否则,使用静态缓冲区。
          顺便说一句,如果传递给便捷方法的缓冲区是局部变量,它们的分配将自动(并且充分)管理,遵循调用便捷方法的子例程的生命周期。
        • 使用循环缓冲区,允许在重复使用缓冲区元素之前进行给定数量的调用。
          这样的缓冲区也可以是全局的,即与多个“便利”方法共享(当然也需要 a)以线程方式运行,并且 b)共享指向缓冲区中下一个可用字节/元素的指针。


        实现这一切似乎是

        • 大量工作(为了方便逻辑),而且
        • 可能存在多个错误/问题

        但是,如果

        • 缓冲区大小合适,并且
        • 使用这些便捷方法的逻辑理解“游戏规则”,

        这种模式提供了一个简单的自动堆管理系统,这在 C 中是一件好事(与 Java 不同,.NET 和其他系统不提供内置的基于 GC 的堆管理)

        【讨论】:

        • 如果数据必须通过多个接口返回和回调持续存在,这听起来很合适,而且开销确实至关重要,并且需要设置,例如在实时驱动程序中。但是与调用者在堆栈上分配缓冲区相比有什么优势呢?这只是要求缓冲区溢出。
        • 有趣的方法。它将以实现这一点及其固有的危险(希望只有一次)为代价提供如此寻求的便利;)
        【解决方案5】:

        如果帮助器的参数始终是文字(如示例中所示),您可以使用宏:

        #define P(NUMLIT) ("number is " #NUMLIT)
        
        ...
        somefunc(somearg, P(6), P(7));
        ...
        

        预处理器从宏参数 NUMLIT 创建一个字符串并将其附加到“number is”以创建单个字符串文字,就像

        "this" " " "string" 
        

        作为单个字符串文字“this string”发出。

        【讨论】:

          【解决方案6】:

          我找到了另一种方法。像这样的:

          #define INIT(n) \
           int xi = 0; \
           char *x[n]; \
          
          #define MACRO(s) \
           (++xi, xi %= sizeof(x)/sizeof(*x), x[xi] = alloca(strlen(s)+1), strcpy(x[xi], (s)), x[xi])
          

          我可以这样称呼:

          INIT(2);
          some_func( somearg, MACRO("testing1"), MACRO("testing2"));
          

          所以缓冲区在堆栈上,不需要任何释放。它甚至是线程安全的。

          【讨论】:

          • 我无耻地把这个当作正确答案,直到有人进来挑战它:)
          • TBH,这是一种奇怪的做法。我会跳过 INIT 宏,只调用 MACRO 两次,每次都在自己的行上。像这样的东西:char * s1 = MACRO("hello"); char * s2 = MACRO("world"); some_func(s1, s2); 再说一次,我会在没有宏的情况下这样做。
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2020-08-04
          • 1970-01-01
          • 2010-11-11
          • 2011-08-04
          相关资源
          最近更新 更多