关于函数返回的字符指针的最佳实践[关闭]

Posted

技术标签:

【中文标题】关于函数返回的字符指针的最佳实践[关闭]【英文标题】:best practice regarding char pointers returned by functions [closed] 【发布时间】:2020-09-14 09:57:45 【问题描述】:

在处理返回指向 C 字符串的 malloc 指针的函数时,最佳实践是什么?

这是一个例子:

FILE *f;
char *tmp;
for (i = 0; i <= 10; i++) 
    tmp = concat(fn, i);
    f = fopen(tmp, "w");
    free(tmp);
    // read f, do something useful ...
    fclose(f);

char* concat(char *a, int b) 返回一个指向新 C 字符串的指针,其中包含 ab 的串联。

我不仅必须指定一个临时指针,然后将其传递给fopen,我还必须每次都传递给free(tmp)。我宁愿喜欢这样的东西:

FILE *f;
char *tmp;
for (i = 0; i <= 10; i++) 
    f = fopen(concat(fn, i), "w");
    // read f, do something useful ...
    fclose(f);

但这当然会导致内存泄漏。那么这里的最佳实践是什么?像concat(char *a, int b, char *result) 这样的结果应该是生成的 C 字符串的预分配内存?此解决方案有其缺点,例如 result 的大小有限或不是最佳大小。

【问题讨论】:

当一个函数返回一个malloced 指针时,调用者必须负责freeing它。 @lurker concat 可以返回一个NULLfopen 不能使用NULL 作为第一个参数,而且free 无法使用concat 的结果接近。 @Youssef13: That is not true. 这是教给学生的常规练习,但既不是 C 标准的强制要求,也不是在所有情况下有益的。在为通用多用户系统编写程序时,如果您要分配内存并在整个程序执行期间保留它,那么最后释放它是没有意义的,这样做可能会对性能。 @EricPostpischil 你应该总是free的主要原因是这样做会暴露程序中其他地方的堆损坏错误。因为在调用free 时会发生崩溃,这使您能够及早发现该错误。链接答案中描述的问题实际上与堆分配无关,而是与某个操作系统如何处理交换文件以及如何设计该特定操作系统的 malloc 和堆 API 的库端口有关。 @EricPostpischil 如果您担心这一点,您可以随时从发布版本中忽略free。一般来说,我怀疑程序在退出时冻结的主要原因是它们从主 GUI 线程进行清理。而不是尽快关闭 GUI,让一些后台线程在后台清理工作,将计算机的控制权交还给用户。这是计算机游戏中的一个主要问题,例如,即使已加载到 RAM 而不是交换文件中,退出时也会冻结。 【参考方案1】:

更坚固的设计:

FILE *open_file_number(const char *str, int number)

    size_t size = strlen(str) + 32;
    char *path = malloc(size);

    if (path == NULL)
    
        return NULL;
    
    snprintf(path, size, "%s%d", str, number);

    FILE *file = fopen(path, "w");

    free(path);
    return file;


for (i = 0; i <= 10; i++)

    FILE *file = open_file_number(some_path, i);

    if (file != NULL)
    
        // Do your stuff
        fclose(file);
    

【讨论】:

【参考方案2】:

这两种方法都在行业中使用。在您的示例中,人们可以假设生成的文件名的最大大小并以这种方式使用本地数组:

for (int i = 0; i <= 10; i++) 
    char filename[1024];
    snprintf(filename, sizeof filename, "%s%d", fn. i);
    FILE *f = fopen(filename, "w");
    if (f != NULL) 
        // read f, do something useful ...
        fclose(f);
     else 
        // report the error?
    

请注意,可以使用if (snprintf(filename, sizeof filename, "%s%d", fn. i) &gt;= (int)sizeof filename) 检测截断。

如果不应该对文件名长度进行假设,或者文件名组合方法更复杂,则返回分配的字符串可能是更合适的选择,但也应该测试内存分配错误:

for (int i = 0; i <= 10; i++) 
    char *filename = concat(fn, i);
    if (filename == NULL)  
       /* handle the error */
       ...
       // break / continue / return -1 / exit(1) ...
    
    FILE *f = fopen(filename, "w");
    if (f == NULL) 
        /* report this error, using `filename` for an informative message */
     else 
        // read f, do something useful...
        // keep `filename` available for other reporting 
        fclose(f);
    
    free(filename);

如果您还没有准备好执行所有这些记录,您可能应该使用具有更精细对象生命周期管理或垃圾收集器的不同语言。


最后,使用 C99 复合文字,您可以定义 concat 以适应您的简化用例:

char *concat(char *dest, const char *s, int b) 
    sprintf(dest, "%s%d", s, b);
    return dest;

#define CONCAT(a, b) concat((char[strlen(a) + 24])"", a, b)

CONCAT 定义了一个未命名的局部变量长度char 适当大小的数组,并在其中构造字符串a 和int b 的串联。我将大小写改为大写,以强调 a 在扩展中被计算两次,因此不应该是一个涉及副作用的表达式。

您可以在第二个代码片段中按预期使用此宏:

    FILE *f;
    char *tmp;
    for (i = 0; i <= 10; i++) 
        f = fopen(CONCAT(fn, i), "w");
        // read f, do something useful ...
        fclose(f);
    

我可能不会推荐这种用法,但这只是我的意见。

【讨论】:

【参考方案3】:

在处理返回指向 C 字符串的 malloc 指针的函数时,最佳实践是什么?

最佳做法:不要使用它们。一个期望调用者free 返回数据的库几乎可以肯定设计得很糟糕,只有极少数例外。我们从 40 年的 C 语言历史中知道这一点,其中糟糕的编写库已经产生了数百万个内存泄漏错误。

合理、有用的库 API 设计的基本规则是:

分配东西的人负责收拾自己的烂摊子。

由于 C 没有 RAII 或构造函数/析构函数,不幸的是,这意味着健全的库需要为您提供清理功能,并且您需要记住调用它。如果它不提供这样的功能,您可能需要考虑编写执行此操作的包装器函数 - 为它们纠正错误的库设计。

如果您是实现该库的人,则应始终尽可能将内存分配留给调用者。传统上,这是通过函数获取指向它写入的缓冲区的指针来完成的。然后将其留给调用者分配足够的内存(如strcpy/strcat),或者提供一个具有最大缓冲区大小的变量,之后函数返回它实际使用的缓冲区的大小(如fgets )。


在您的示例中,精心设计的 concat 可能看起来像

const char* concat (char* restrict dst, const char* restrict src, int i);

其中src 是源字符串,i 是要添加的整数,dst 是调用者提供的足够大的缓冲区。可选地,为方便起见,该函数返回一个等效于dst 的指针。上面还实现了正确的 const 正确性 加上一个带限制的微优化,这意味着传递的指针不允许重叠。

用法:

char buf [LARGE_ENOUGH];
fp = fopen(concat(buf, foo, i), "w");

【讨论】:

具有副作用并返回值的函数是另一个错误来源(即使在 C 编程语言中很常见)。 @AugustKarlstrom 当然,但这与这个答案有什么关系?我发布的功能应该是完全可重入的。 我会使用 void 作为 concat 的返回类型来提高代码的可读性,仅此而已。 @AugustKarlstrom 是的,我也更喜欢这种风格,但是如果函数没有副作用并且返回的字符串是const 限定的,那也没什么坏处。 危害在于,当对 concat 的调用嵌入到表达式中时,您可能会错过这样一个事实,即第一个参数是输出参数。在这种情况下,同一个语句中的内容太多了。【参考方案4】:

您的第一个代码 sn-p,在您使用完后保存返回的指针和 free 它,是使用返回 malloc 内存的函数的正确方法。

有几个 POSIX 函数,例如 strdupgetline,以这种方式工作,所以这是一个众所周知的习语。

替代方案是:

返回指向静态缓冲区的指针。这样做的缺点是它不是线程安全的,也不能在一个表达式中调用两次。 接受一个指向适当大小缓冲区的指针,在这种情况下,适当大小的缓冲区取决于调用者。

【讨论】:

嗯...“这是一个众所周知的习语”->“这是一个众所周知的内存泄漏源”:) 有很多古老的 UNIX 垃圾函数和可怕的 API 仍然存在在。其中一些甚至进入了 C 标准库。仅仅因为它们一直存在并不会使它们成为惯用语。【参考方案5】:

如果您知道字符串的最大大小,您还可以执行以下操作:

char* concat_(char* buf, size_t s, char *a, int b)

    /* ... your code ... */
    return buf;


#define concat(a, b)    concat_((char[100])"", 100, (a), (b))

宏分配一个未命名的局部变量并将其传递给concat_ 函数。然后这个函数可以做它以前做过的任何事情,只返回指向同一个对象的指针。没有malloc,没有free,不用担心(除了可能会炸毁你的堆栈,如果你让缓冲区太大)。

编辑: 请注意,正如 Gerhardh 在 cmets 中指出的那样,这会创建一个 local 对象(具有自动存储持续时间),因此宏返回的指针仅在实际调用该宏的同一块。因此,例如,您不能将该指针用作调用宏的函数的返回值。

【讨论】:

"没有 malloc,没有免费,不用担心" 好吧,你不能返回那个宏的结果,因为那个局部变量会结束存在。 @Gerhardh 当然,但这就是为什么我写了“未命名的 local 变量”而不是“复合文字”,因为即使对初学者来说也应该很明显,你不会传递指向局部变量的指针 如果您按照 SO 上的问题进行操作,您很快就会发现,这并不像您想象的那么明显。 ;) 这个问题每周至少出现两次。这也是一个明确的限制,应该明确提及。 在超过 100 神奇数字之前不用担心,也就是说...我真的不建议将缓冲区大小隐藏在某个宏后面。 @Gerhardh 在这种情况下,复合文字将与封闭的调用者块具有相同的范围。它不是函数本地的,它是调用者本地的。

以上是关于关于函数返回的字符指针的最佳实践[关闭]的主要内容,如果未能解决你的问题,请参考以下文章

Delphi 的内存操作函数: 给字符指针分配内存( 给字符指针(PCharPWideCharPAnsiChar)分配内存最佳的选择是StrAlloc。分配内存的时候会对字符串进行初始化)

c语言函数返回字符串时必须要用指针吗?如果返回结构体呢?函数在返回那些类型值时必须要用指针?

用指针影响字符串[关闭]

关于指针数组字符串的恩怨,这里有你想知道的一切

用指针影响字符串[关闭]

从函数返回堆字符指针的问题