【问题标题】:Return dynamically allocated memory from C++ to C将动态分配的内存从 C++ 返回到 C
【发布时间】:2008-11-12 08:18:50
【问题描述】:

我有一个必须可以从 C 等语言中使用的 dll,所以我不能像往常一样使用字符串对象等,但我不确定如何安全地执行此操作..

const char *GetString()
{
    std::stringstream ss;
    ss << "The random number is: " << rand();
    return ss.str().c_str();
}

当 ss 从堆栈中掉下来时,c 字符串会被销毁吗?我猜是……

另一种选择可能是在堆上创建一个新字符串,但如何释放它?

const char *GetString()
{
    std::stringstream ss;
    ss << "The random number is: " << rand();
    char *out = new char[ss.str().size()];
    strcpy(ss.str().c_str(), out);
    return out;//is out ever deleted?
}

指向其他事物的指针和字符串也是如此。

【问题讨论】:

    标签: c++ c memory-management


    【解决方案1】:

    第一个变体不起作用,因为您要返回一个指向堆栈对象的指针,该堆栈对象将被销毁。 (更确切地说,您返回一个指向堆内存的指针,该指针将被删除()。)更糟糕的是,如果没有人覆盖内存,它甚至可能工作一段时间,这使得调试变得非常困难。

    接下来,你不能返回一个 const char*,除非你像这样返回一个指向静态字符串的指针:

    const char *GetString()
    {
        return "a static string in DATA segment - no need to delete";
    }
    

    您的第二个变体存在将使用 new() 分配的内存返回到将调用 free() 的 C 程序的问题。这些可能不兼容。

    如果你将一个字符串返回给 C,有两种方法可以做到这一点:

    char *GetString()
    {
        std::stringstream ss;
        ss << "The random number is: " << rand();
        return strdup( ss.str().c_str() ); // allocated in C style with malloc()
    }
    
    void foo()
    {
        char *p = GetString();
        printf("string: %s", p));
        free( p ); // must not forget to free(), must not use delete()
    }
    

    或:

    char *GetString(char *buffer, size_t len)
    {
        std::stringstream ss;
        ss << "The random number is: " << rand();
        return strncpy(buffer, ss.str().c_str(), len); // caller allocates memory
    }
    
    void foo()
    {
        char buffer[ 100 ];
        printf("string: %s", GetString(buffer, sizeof( buffer ))); // no memory leaks
    }
    

    取决于您的内存处理策略。

    通常,您永远不能在 C++ 中返回指向自动对象的指针或引用。这是许多 C++ 书籍中分析的常见错误之一。

    【讨论】:

      【解决方案2】:

      多年来,C 将其归结为 2 种标准方法:

      • 调用者传入缓冲区。
        这有三个版本。
        版本 1:传递缓冲区和长度。
        版本 2:文档指定了预期的最小缓冲区大小。
        第 3 版:飞行前。函数返回所需的最小缓冲区。调用者第一次使用 NULL 缓冲区调用两次。
        • 示例:read()
      • 使用在下一次调用前有效的静态缓冲区。
        • 示例:tmpname()

      一些非标准的返回你必须明确释放的内存

      • strdup() 突然出现在脑海中。
        通用扩展,但实际上不在标准中。

      【讨论】:

        【解决方案3】:

        第一个实际上不起作用,因为字符串流在销毁时释放了它的空间。因此,如果您尝试取消引用该指针,那么您的程序很可能会崩溃。

        您提到的第二个选项是它通常是如何完成的,并且该函数的用户需要释放空间。如果这是一个使用该函数的 C 程序,请确保使用 malloc() 分配并使用 free() 释放

        另一种选择是返回静态字符数组的地址。如果您事先知道长度的良好上限,则这是相关的。更重要的是,只有在不可能同时从两个不同的线程调用函数时才应该使用它,因为使用静态数组本质上会使您的函数非reentrant

        【讨论】:

        • 好的,我的 dll 确实适用于 python,那么最好的方法是什么?我应该将 dll 函数包装在说“调用 dll 函数;调用 dll 释放函数”的 python 函数中吗?我假设 python 制作了一个全新的字符串,而不是仅仅在它周围包裹一个对象?
        【解决方案4】:

        很明显,每当您返回指向函数内部分配的内存的指针时,解除分配必须来自外部,除非您使用垃圾回收。如果您不想这样做,请在调用 GetString() 之前分配一个字符缓冲区并将原型更改为

        int get_string(const char* buffer);

        然后填满缓冲区。但是将一个点返回给分配的数据是可以的。

        【讨论】:

        • 请同时传递缓冲区的大小!
        【解决方案5】:

        如果您将 ss 声明为静态,则可以避免该问题。如果您的程序在单线程环境中运行,这可能是一个很好的解决方案。

        【讨论】:

        • 但是一旦再次调用该函数,结果就会无效。从那一刻起,只有第二次调用的结果有效。
        • 让尖叫声变得静止会使事情变得更糟。他们会分享字符串吗?流是否保证指针不会改变(否)?它会删除重新分配的指针(是)吗?在第二次调用后,第一个调用者将留下一个死指针,等等。不去
        【解决方案6】:

        如果你想安全地返回它,你必须在堆上分配字符串,也可以使用 malloc() i.s.o 进行分配。 new() 在编写 C 函数时。

        当你返回指针时(而且,与 C++ 不同,在 C 中你很多时候没有真正的选择),释放总是一个问题。没有真正的明确解决方案。

        我在很多 API 中看到的一种处理方法是调用所有函数

        CreateString()
        

        当调用者需要释放内存时,以及

        GetString()
        

        当这不是问题时。

        当然,这绝不是万无一失的,但如果有足够的纪律,这是我见过的最好的方法,老实说......

        【讨论】:

          【解决方案7】:

          如果线程安全不重要,

          const char *GetString()
          {
              static char *out;
              std::stringstream ss;
              ss << "The random number is: " << rand();
              delete[] out;
              char *out = new char[ss.str().size()];
              strcpy(ss.str().c_str(), out);
              return out;//is out ever deleted?
          }
          

          然后函数可以接管解除分配字符串的责任。

          如果线程安全很重要,

          那么最好的方法就是将其作为参数传入,如,

          void GetString(char *out, int maxlen);
          

          我观察到当旧的非线程安全 API 更改为线程安全时会发生这种情况。

          【讨论】:

          • 1) 产生静态的原因是什么? 2) 你不能返回 const char* 3) 你不能将新的内存返回到 C 程序中
          • 1) 以便在调用 GetString 之间保留最后一个新内存的地址。 2)好的,同意C没有const。然后返回 char *。 3)只要C程序不调用free就没有关系。
          【解决方案8】:

          调用函数后,您会希望调用者负责字符串的内存(尤其是取消分配它)。除非你想使用静态变量,但有龙!干净利落的最好方法是让调用者首先分配内存:

          void foo() {
            char result[64];
            GetString(result, sizeof(result));
            puts(result);
          }

          然后 GetString 应该如下所示:

          int GetString(char * dst, size_t len) {
            std::stringstream ss;
            ss << "The random number is: " << rand();
            strncpy(ss.str().c_str(), dst, len);
          }

          传递最大缓冲区长度并使用 strncpy() 将避免意外覆盖缓冲区。

          【讨论】:

            【解决方案9】:

            到目前为止的答案并没有解决一个非常重要的问题,即如果结果所需缓冲区的长度未知并且可以在调用之间更改,即使使用相同的参数(例如从一个数据库),所以我提供了我认为是处理这种情况的最佳方法。

            如果事先不知道大小,请考虑将回调函数传递给您的函数,该函数接收const char* 作为参数:

            typedef void (*ResultCallback)( void* context, const char* result );
            
            void Foo( ResultCallback resultCallback, void* context )
            {
                 std::string s = "....";
                 resultCallback( context, s.c_str() );
            }
            

            ResultCallback的实现可以分配需要的内存并复制result指向的缓冲区。我假设是 C,所以我没有明确地向/从 void* 转换。

            void UserCallback( void* context, const char* result )
            {
                char** copied = context;
                *copied = malloc( strlen(result)+1 );
                strcpy( *copied, result );
            }
            
            void User()
            {
                char* result = NULL;
            
                Foo( UserCallback, &result );
            
                // Use result...
                if( result != NULL )
                    printf("%s", result);
            
                free( result );
            }
            

            这是最便携的解决方案,甚至可以处理无法预先知道返回字符串大小的最棘手的情况。

            【讨论】:

              【解决方案10】:

              随着时间的推移,有多种方法可以从函数返回可变数量的数据。

              1. 调用者传入缓冲区。
                1. 记录了必要的大小但未通过,缓冲区太短为Undefined Behavior:strcpy()
                2. 记录并传递必要的大小,返回值指示错误:strcpy_s()
                3. 需要的大小未知,但可以通过调用缓冲区长度为0的函数来查询:snprintf
                4. 所需大小未知且无法查询,返回的大小与传递大小的缓冲区中的大小相同。如有必要,必须拨打其他电话以获取其余信息:fread
                5. 所需大小未知,无法查询,传递的缓冲区太小为Undefined Behavior。这是一个设计缺陷,因此该功能在较新的版本中已被弃用/删除,为了完整起见,此处仅提及:gets
              2. 调用者传递回调:
                1. 回调函数获取上下文参数:qsort_s
                2. 回调函数没有上下文参数。获取上下文需要魔法:qsort
              3. 调用者传递一个分配器:在 C 标准库中找不到。不过,所有可识别分配器的 C++ 容器都支持这一点。
              4. 被调用者合约指定解除分配器。打错的是Undefined Behaviorfopen->fclosestrdup->free
              5. 被调用者返回一个包含释放器的对象:COM-Objects std::shared_ptr
              6. 被调用者使用内部共享缓冲区:asctime

              一般来说,每当用户必须猜测尺寸或在手册中查找时,他有时会弄错。如果他没有弄错,以后的修改可能会使他的仔细工作失效,所以他曾经是对的也没关系。反正这边是madness (UB)

              对于其余的,请选择最舒适和最有效的。

              【讨论】:

                猜你喜欢
                • 2017-12-13
                • 2017-10-06
                • 1970-01-01
                • 1970-01-01
                • 2015-12-07
                • 2015-06-27
                • 2021-02-28
                • 2012-01-20
                相关资源
                最近更新 更多