【问题标题】:best practice for returning a variable length string in c在 c 中返回可变长度字符串的最佳实践
【发布时间】:2013-06-08 22:00:54
【问题描述】:

我有一个字符串函数,它接受指向源字符串的指针并返回指向目标字符串的指针。此功能目前有效,但我担心我没有遵循重新分级 malloc、realloc 和 free 的最佳做法。

我的函数的不同之处在于目标字符串的长度与源字符串的长度不同,因此必须在我的函数内部调用 realloc()。我通过查看文档知道...

http://www.cplusplus.com/reference/cstdlib/realloc/

内存地址在重新分配后可能会改变。这意味着我不能像 C 程序员那样“通过引用传递”其他函数,我必须返回新指针。

所以我的函数原型是:

//decode a uri encoded string
char *net_uri_to_text(char *);

我不喜欢我这样做的方式,因为我必须在运行函数后释放指针:

char * chr_output = net_uri_to_text("testing123%5a%5b%5cabc");
printf("%s\n", chr_output); //testing123Z[\abc
free(chr_output);

这意味着 malloc() 和 realloc() 在我的函数内部调用,而 free() 在我的函数外部调用。

我有高级语言(perl、plpgsql、bash)的背景,所以我的直觉是适当封装这些东西,但这可能不是 C 语言的最佳实践。

问题:我的方法是最佳实践,还是我应该遵循更好的方法?

完整示例

编译和运行时对未使用的 argc 和 argv 参数有两个警告,您可以放心地忽略这两个警告。

example.c:

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

char *net_uri_to_text(char *);

int main(int argc, char ** argv) {
  char * chr_input = "testing123%5a%5b%5cabc";
  char * chr_output = net_uri_to_text(chr_input);
  printf("%s\n", chr_output);
  free(chr_output);
  return 0;
}

//decodes uri-encoded string
//send pointer to source string
//return pointer to destination string
//WARNING!! YOU MUST USE free(chr_result) AFTER YOU'RE DONE WITH IT OR YOU WILL GET A MEMORY LEAK!
char *net_uri_to_text(char * chr_input) {
  //define variables
  int int_length = strlen(chr_input);
  int int_new_length = int_length;
  char * chr_output = malloc(int_length);
  char * chr_output_working = chr_output;
  char * chr_input_working = chr_input;
  int int_output_working = 0;
  unsigned int uint_hex_working;
  //while not a null byte
  while(*chr_input_working != '\0') {
    //if %
    if (*chr_input_working == *"%") {
      //then put correct char in
      sscanf(chr_input_working + 1, "%02x", &uint_hex_working);
      *chr_output_working = (char)uint_hex_working;
      //printf("special char:%c, %c, %d<\n", *chr_output_working, (char)uint_hex_working, uint_hex_working);
      //realloc
      chr_input_working++;
      chr_input_working++;
      int_new_length -= 2;
      chr_output = realloc(chr_output, int_new_length);
      //output working must be the new pointer plys how many chars we've done
      chr_output_working = chr_output + int_output_working;
    } else {
      //put char in
      *chr_output_working = *chr_input_working;
    }
    //increment pointers and number of chars in output working
    chr_input_working++;
    chr_output_working++;
    int_output_working++;
  }
  //last null byte
  *chr_output_working = '\0';
  return chr_output;
}

【问题讨论】:

  • 我喜欢*"%" 部分。 :D
  • 谢谢,我刚刚发现'%' 有效。 :)
  • 如果你在反引号(`)之间包裹你的代码,那么它们将被格式化为代码
  • 这个int int_length = strlen(chr_input); ... char * chr_output = malloc(int_length); 看起来很危险。如果字符串在转换过程中没有收缩,则分配的内存太短了一个字节。字符串的0-terminator 无法保存。
  • 那么char * chr_output = malloc(int_length + 1); 是正确的,这一行将被更改:chr_output = realloc(chr_output, int_new_length); 到 chr_output = realloc(chr_output, int_new_length + 1); 对吗?

标签: c string function


【解决方案1】:

从 C 中的函数返回 malloc'd 缓冲区是完全可以的,只要你记录他们这样做的事实。很多库都这样做,即使标准库中没有函数这样做。

如果您可以廉价地计算(一个不太悲观的上限)需要写入缓冲区的字符数,您可以提供一个函数来执行此操作并让用户调用它。

也可以接受要填充的缓冲区,但不太方便;我见过很多这样的库:

/*
 * Decodes uri-encoded string encoded into buf of length len (including NUL).
 * Returns the number of characters written. If that number is less than len,
 * nothing is written and you should try again with a larger buffer.
 */
size_t net_uri_to_text(char const *encoded, char *buf, size_t len)
{
    size_t space_needed = 0;

    while (decoding_needs_to_be_done()) {
        // decode characters, but only write them to buf
        // if it wouldn't overflow;
        // increment space_needed regardless
    }
    return space_needed;
}

现在调用者负责分配,并且会做类似的事情

size_t len = SOME_VALUE_THAT_IS_USUALLY_LONG_ENOUGH;
char *result = xmalloc(len);

len = net_uri_to_text(input, result, len);
if (len > SOME_VALUE_THAT_IS_USUALLY_LONG_ENOUGH) {
    // try again
    result = xrealloc(input, result, len);
}

(这里,xmalloc 和 xrealloc 是我为跳过 NULL 检查而编写的“安全”分配函数。)

【讨论】:

  • 期望调用者传递缓冲区(+ 缓冲区大小)的两个很好的方面: 1. 调用者可能预先知道最大长度,因此他可以决定使用分配在堆栈上的数组. 2.) 内存的所有权不会被转移,即分配和释放都发生在调用者站点上——这在 Windows 上很重要,以防调用者与被调用者位于不同的 DLL 中(禁止在一个 DLL 中分配内存并在 Windows 上将其发布到另一个中,因为内存管理器是按模块而不是按进程的)。
  • @FrerichRaabe:我不知道。我确实知道它允许使用自定义内存管理器而不是 malloc,这在 Unix 上也很有用。
【解决方案2】:

问题是 C 语言非常低级,足以迫使程序员正确地进行内存管理。特别是,返回 malloc()ated 字符串并没有错。返回分配错误的对象并让调用者free() 他们是一种常见的习惯用法。

无论如何,如果您不喜欢这种方法,您可以随时获取指向字符串的指针并从函数内部对其进行修改(但在最后一次使用后,它仍需要为 free()d) .

但是,我认为没有必要的一件事是显式缩小字符串。如果新字符串比旧字符串短,那么在旧字符串的内存块中显然有足够的空间放置它,所以你不需要realloc()。

(除了你忘记为终止 NUL 字符分配一个额外的字节,当然......)

而且,与往常一样,每次调用函数时,您都可以返回不同的指针,甚至根本不需要调用 realloc()。

如果您接受最后一条好建议:建议const-限定您的输入字符串,以便调用者可以确保您不会修改它们。例如,使用这种方法,您可以安全地在字符串文字上调用该函数。

总而言之,我会这样重写你的函数:

char *unescape(const char *s)
{
    size_t l = strlen(s);
    char *p = malloc(l + 1), *r = p;

    while (*s) {
        if (*s == '%') {
            char buf[3] = { s[1], s[2], 0 };
            *p++ = strtol(buf, NULL, 16); // yes, I prefer this over scanf()
            s += 3;
        } else {
            *p++ = *s++;
        }
    }

    *p = 0;
    return r;
}

并调用如下:

int main()
{
    const char *in = "testing123%5a%5b%5cabc";
    char *out = unescape(in);
    printf("%s\n", out);
    free(out);

    return 0;
}

【讨论】:

  • Welp,我没有明确提到size_t 和strtol(),而且我还假设malloc() 永远不会失败...小心!
【解决方案3】:

从函数返回新的-malloc-ed(也可能是内部的realloced)值是完全可以的,您只需要记录您正在这样做(就像您在此处所做的那样)。

其他明显的项目:

  • 您可能想要使用size_t 而不是int int_length。这是“无符号类型”(通常是unsigned int 或unsigned long),它是malloc 的字符串长度和参数的适当类型。
  • 最初需要分配 n+1 个字节,其中 n 是字符串的长度,因为strlen 不包括终止的 0 字节。
  • 您应该检查 malloc 是否失败(返回 NULL)。如果您的函数将传递失败,请在函数描述注释中记录。
  • sscanf 非常适合转换两个十六进制字节。没有错,只是您没有检查转换是否成功(如果输入格式错误怎么办?您当然可以确定这是调用者的问题,但通常您可能想要处理) .您可以使用 &lt;ctype.h&gt; 中的 isxdigit 检查十六进制数字,和/或使用 strtoul 进行转换。
  • 与其为每个% 转换执行一次realloc,不如根据需要执行最终的“收缩重新分配”。请注意,如果您为一个字符串分配(比如说)50 个字节,但发现它只需要 49 个字节,包括最后的 0 个字节,那么可能不值得做 realloc。

【讨论】:

    【解决方案4】:

    我会以稍微不同的方式解决这个问题。就个人而言,我会将您的功能一分为二。第一个函数计算你需要malloc的大小。第二个将输出字符串写入给定指针(已在函数外部分配)。这节省了对 realloc 的多次调用,并且将保持相同的复杂性。查找新字符串大小的可能函数是:

    int getNewSize (char *string) {
        char *i = string;
        int size = 0, percent = 0;
        for (i, size; *i != '\0'; i++, size++) {
            if (*i == '%')
                percent++;
        }
        return size - percent * 2;
    }
    

    但是,正如其他答案中提到的,只要您记录它,返回 malloc 的缓冲区就没有问题!

    【讨论】:

    • 请注意,如果您决定像这样重构,您可以让“计算空间的函数”也验证 URL 格式是否正确(没有类似%-! 在中间)。有时人们也会选择一种混合方法:验证,可选地 malloc,可选地转换为缓冲区(无论是用户提供的还是 malloc-ed),返回一堆信息(通过struct 或调用者提供的指针),等等。
    【解决方案5】:

    除了其他帖子中已经提到的内容之外,您还应该记录字符串被重新分配的事实。如果您的代码使用静态字符串或使用alloca 分配的字符串调用,您可能不会重新分配它。

    【讨论】:

      【解决方案6】:

      我认为您担心拆分 malloc 和 free 是正确的。作为一项规则,无论制造它、拥有它并应该释放它。

      在这种情况下,字符串相对较小,一个好的方法是使字符串缓冲区大于它可能包含的任何可能的字符串。例如,URL 的实际限制约为 2000 个字符,因此如果您 malloc 10000 个字符,则可以存储任何可能的 URL。

      另一个技巧是将字符串的长度和容量都存储在它的前面,这样字符串的(int)*mystring == length of string 和(int)*(mystring + 4) == capacity。因此,字符串本身仅从第 8 个位置 *(mystring+8) 开始。通过这样做,您可以传递一个指向字符串的指针,并且始终知道它有多长以及该字符串有多少内存容量。您可以制作自动生成这些偏移量并制作“漂亮代码”的宏。

      以这种方式使用缓冲区的价值是您不需要进行重新分配。新值覆盖旧值,并更新字符串开头的长度。

      【讨论】:

      • 我认为“在前几个字节中编码字符串的大小”技术非常讨厌,因为它相当不常见(我在实践中从未见过),如果你忘记了,编译器也无能为力这个事实。因此,如果您有一个字符串“Hello”,打印它可能只会打印一个字符,因为初始 int 的大多数字节都是零。调试不是一件好事。 :-/
      • 当您使用字符串的内容时,您将其称为 '*(mystring+8)'。例如: printf( "%s\n", *(mystring+8) );如果需要,可以使用宏。另一种方法是定义一个结构,但是你必须处理嵌套指针。根据我的经验,在处理短字符串时,使用我描述的方法比使用嵌套指针更容易。
      猜你喜欢
      • 2014-01-23
      • 1970-01-01
      • 2018-12-18
      • 1970-01-01
      • 2022-08-10
      • 2014-12-31
      • 2010-12-23
      • 2017-09-07
      • 1970-01-01
      相关资源
      最近更新 更多