【问题标题】:Using C, convert a dynamically-allocated int array to a comma-separated string as cleanly as possible使用 C,将动态分配的 int 数组尽可能干净地转换为逗号分隔的字符串
【发布时间】:2010-12-17 06:40:44
【问题描述】:

我在 C 方面的经验远不如我在高级语言方面的经验。在思科,我们使用 C,有时我会遇到一些在 Java 或 Python 中很容易做到但在 C 中很难做到的事情。现在就是其中之一。

我有一个动态分配的无符号整数数组,我需要将其转换为逗号分隔的字符串以进行日志记录。虽然整数不太可能很大,但从概念上讲,它们可以是 0 到 4,294,967,295 之间的任何值。在 Python 中,这只是很短的一行。

my_str = ','.join(my_list)

人们可以在 C 中优雅地做到这一点吗?我想出了一个办法,但它很恶心。如果有人知道一个很好的方法,请赐教。

【问题讨论】:

  • 你能发布你在 C 中的做法吗?
  • 等等,join()是string类的成员函数!?
  • Bipedal:这样我们只需要实现一次连接(好吧,每个字符串类一次,所以两次),而不是对每种可能的可迭代对象类型执行数百次。将','.join(sequence) 读作“逗号分隔序列”,将''.join(sequence) 读作“空分隔序列”
  • Bipedal:是的,实际上是这样。 Python 使用鸭子类型,任何对象都可以通过仅实现 next 和 __iter__ 方法成为迭代器。 Python 不需要继承来指定接口。

标签: c string


【解决方案1】:

代码现在在 gcc 下测试和构建。

与其他答案相比,不强制使用 C99。

这里真正的问题是不知道你需要的字符串的长度。获得一个数字就像sprintf("%u", *num) 使用num 遍历您的ints 数组一样简单,但是您需要多少空间?为避免溢出缓冲区,您必须跟踪大量整数。

size_t join_integers(const unsigned int *num, size_t num_len, char *buf, size_t buf_len) {
    size_t i;
    unsigned int written = 0;

    for(i = 0; i < num_len; i++) {
        written += snprintf(buf + written, buf_len - written, (i != 0 ? ", %u" : "%u"),
            *(num + i));
        if(written == buf_len)
            break;
    }

    return written;
}

请注意,我会跟踪我使用了多少缓冲区并使用了snprintf,所以我不会超出结尾。 snprintf 将添加 \0,但由于我使用的是 buf + written,我将从之前的 snprintf\0 开始。

使用中:

int main() {
    size_t foo;
    char buf[512];

    unsigned int numbers[] = { 10, 20, 30, 40, 1024 };

    foo = join_integers(numbers, 5, buf, 512);
    printf("returned %u\n", foo);
    printf("numbers: %s\n", buf);
}

输出:

returned 20
numbers: 10, 20, 30, 40, 1024

强制限制生效,而不是超限:

char buf[15];    
foo = join_integers(numbers, 5, buf, 14);
buf[14] = '\0';

预期的输出:

returned 14
numbers: 10, 20, 30, 4

【讨论】:

  • 为什么说它不强制要求C99? snprintf 来自 C99。
  • 从技术上讲,snprintf() 是 C99 函数,而不是 C89。但是,它足够广泛可用,不会成为主要问题。此外,使用 sizeof(buf) 而不是 512,即使在测试代码中也是如此。
  • 我想有人可能会为 snprintf() 在 SUSV2 中的某些平台争论。
  • @Jonathan Leffler:那为什么不在函数内部使用 sizeof(buf) 呢?
  • @Spidey:你不能在函数内使用'sizeof(buf)',因为C传递指针,所以给定的大小将是指针的大小(可能是4或8)而不是它指向的数组的大小。
【解决方案2】:

您实际上可以使用像Glib 这样的库,其中包含类似的函数

gchar* g_strjoin (const gchar *分隔符, ...);

将多个字符串连接在一起形成一个长字符串,并在每个字符串之间插入可选的分隔符。应该使用 g_free() 释放返回的字符串。

(您仍然需要使用g_snprintf(),可能还需要使用g_printf_string_upper_bound() 以确保空间)

【讨论】:

    【解决方案3】:

    你们是按线路获得报酬的吗? :-)


    f() 使用char * 参数声明用于原型设计,只需更改char -&gt; int。我将这个问题解释为需要一个字符串作为输出,而不仅仅是写入文件的代码。

    #define PRINT(s, l, x, i) snprintf((s), (l), "%s%d", (i) ? ",":"", (x)[i]);
    
    char *f(size_t len, char *x) {
      size_t  i, j = 0, k;
    
      for(i = 0; i < len; ++i)
          j += PRINT(NULL, 0, x, i);
      char *result = malloc(k = ++j);
      for(*result = i = j = 0; i < len; ++i)
          j += PRINT(result + j, k - j, x, i);
      return result;
    }
    

    这是一个测试框架:

    #include <stdlib.h>
    #include <stdio.h>
    #include <string.h>
    
    // put f() here
    
    int main(int ac, char **av) {
        for(int i = 1; i < ac; ++i) { 
            printf("%s\n", f(strlen(av[i]), av[i]));
        }
        return 0;
    }
    

    【讨论】:

    • 如果您愿意使用静态分配的临时缓冲区,只需摆脱第一个循环...
    【解决方案4】:

    这个呢?

    char *join_int_list(const unsigned int *list, size_t n_items)
    {
         enum { SIZEOF_INT_AS_STR = sizeof("4294967295,")-1 };
         char *space = malloc(SIZEOF_INT_AS_STR * n_items);
         if (space != 0)
         {
             size_t i;
             char *pad = "";
             char *dst = space;
             char *end = space + SIZEOF_INT_AS_STR * n_items;
             for (i = 0; i < n_items; i++)
             {
                  snprintf(dst, end - dst, "%s%u", pad, list[i]);
                  pad = ",";
                  dst += strlen(dst);
             }
             space = realloc(space, dst - space + 1);
         }
         return(space);
    }
    

    调用者有责任释放返回的指针 - 并在使用它之前检查它是否不为空。如果分配的数量太大而无法使用,则“realloc()”会释放额外的空间。这段代码愉快地假设这些值确实是 32 位无符号整数;如果它们可以更大,那么枚举需要适当的调整。

    测试代码:

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    char *join_int_list(const unsigned int *list, size_t n_items)
    {
        enum { SIZEOF_INT_AS_STR = sizeof("4294967295,")-1 };
        char *space = malloc(SIZEOF_INT_AS_STR * n_items);
        if (space != 0)
        {
            size_t i;
            char *pad = "";
            char *dst = space;
            char *end = space + SIZEOF_INT_AS_STR * n_items;
            for (i = 0; i < n_items; i++)
            {
                snprintf(dst, end - dst, "%s%u", pad, list[i]);
                pad = ",";
                dst += strlen(dst);
            }
            space = realloc(space, dst - space + 1);
        }
        return(space);
    }
    
    int main(void)
    {
        static unsigned int array[]= { 1, 2, 3, 49, 4294967295U, 0, 332233 };
        char *str = join_int_list(array, sizeof(array)/sizeof(array[0]));
        printf("join: %s\n", str);
        free(str);
        return(0);
    }
    

    用 valgrind 检查 - 似乎没问题。


    讨论INT_MAXUINT_MAX转字符串:

    您可以使用 sizeof("," STRINGIZE(INT_MAX)) 代替硬编码。 stringize 宏是一个常用的 cpp 工具,可以定义为 #define STRINGIZE_(v) #v 和 #define STRINGIZE(v) STRINGIZE_(v)。 – R.佩特

    @R Pate:好主意 - 是的,你可以非常有效地做到这一点。实际上,其中有两个有趣的想法:使用带有 sizeof() 的字符串连接(为了清楚起见需要括号 - 但字符串连接发生得足够早,编译器不会担心)以及在 INT_MAX 上使用字符串化操作. ——乔纳森·莱弗勒

    INT_MAX 使用字符串化操作不是一个好主意——它必须是一个“常量表达式”,而不一定是数字序列。它可以定义为 ((1

    @caf 是对的。考虑这段代码:

    #include <limits.h>
    #include <stdio.h>
    
    #undef INT_MAX
    #define INT_MAX (INT_MIN-1 - 100 + 100)
    
    #define QUOTER(x)   #x
    #define STRINGIZER(x)   QUOTER(x)
    
    enum { SIZEOF_INT_AS_STR = sizeof("4294967295,")-1 };
    enum { SIZEOF_INT_AS_STR_1 = sizeof(STRINGIZER(INT_MAX) ",")-1 };
    
    int main(void)
    {
        printf("size = %d = %d\n", SIZEOF_INT_AS_STR, SIZEOF_INT_AS_STR_1);
        printf("INT_MAX  = %d\n", INT_MAX);
        printf("UINT_MAX = %u\n", UINT_MAX);
        return(0);
    }
    

    这甚至不能在带有 GCC 4.0.1 的 MacOS X 10.5.8 上编译 - 因为标识符 INT_MAX 没有定义。未打印 INT_MAXUINT_MAX 的代码的初步版本有效;它表明 SIZEOF_INT_AS_STR_1 的值是 31 - 所以 @caf 是正确的。添加对INT_MAXUINT_MAX 值的双重检查然后编译失败,这让我感到惊讶。看看gcc -E 的输出就会发现原因:

    enum { SIZEOF_INT_AS_STR = sizeof("4294967295,")-1 };
    enum { SIZEOF_INT_AS_STR_1 = sizeof("((-INT_MAX - 1)-1 - 100 + 100)" ",")-1 };
    
    int main(void)
    {
     printf("size = %d = %d\n", SIZEOF_INT_AS_STR, SIZEOF_INT_AS_STR_1);
     printf("INT_MAX  = %d\n", ((-INT_MAX - 1)-1 - 100 + 100));
     printf("UINT_MAX = %u\n", (((-INT_MAX - 1)-1 - 100 + 100) * 2U + 1U));
     return(0);
    }
    

    正如预测的那样,SIZEOF_IN_AS_STR_1 的字符串根本不是数字字符串。预处理器可以评估表达式(尽可能多地),但不必生成数字字符串。

    INT_MAX 的扩展原来是根据INT_MIN 来定义的,而INT_MIN 反过来又是根据INT_MAX 定义的,所以当评估重写的INT_MAX 宏时,' C 预处理器的操作规则阻止了递归扩展,并且 INT_MAX 出现在预处理输出中 - 让所有人感到困惑。

    因此,表面上吸引人的想法被证明是个坏主意的原因有很多。

    【讨论】:

    • 请注意,SIZEOF_INT_AS_STR 仅对具有 32 位整数的系统有效。
    • 是的:为此添加了注释,但提问者引用的范围有效地执行了这一点,因此它符合规范。
    • 您可以使用sizeof("," STRINGIZE(INT_MAX)) 而不是硬编码。 stringize宏是一个常用的cpp工具,可以定义为#define STRINGIZE_(v) #v#define STRINGIZE(v) STRINGIZE_(v)
    • @R Pate:好主意 - 是的,你可以非常有效地做到这一点。实际上,其中有两个有趣的想法:使用sizeof() 进行字符串连接(为了清楚起见需要括号 - 但字符串连接发生得足够早,编译器不会担心)以及在INT_MAX 上使用字符串化操作.
    • INT_MAX 上使用字符串化操作不是一个好主意——它必须是一个“常量表达式”,而不一定是一个数字序列。它可以定义为((1&lt;&lt;32)-1),甚至可以定义为__int_max 之类的古怪名称,只要编译器允许您在可以使用常量表达式的任何地方使用它。
    【解决方案5】:
    unsigned *a; /* your input a[N] */
    unsigned i,N;
    char *b,*m;
    b=m=malloc(1+N*11); /* for each of N numbers: 10 digits plus comma (plus end of string) */
    for (i=0;i<N;++i)
      b+=sprintf(b,"%u,",a[i]);
    if (N>0) b[-1]=0; /* delete last trailing comma */
    /* now use m */
    free(m);
    

    很漂亮,对吧? :)

    【讨论】:

      【解决方案6】:
      char buf [11 * sizeof (my_list)];
      for (int n = 0, int j = 0;  j < sizeof (my_list) / sizeof (my_list [0]);  ++j)
          n += sprintf (&buf [n], "%s%u",   (j > 0) ? "," : "",  my_list [j]);
      

      【讨论】:

      • 这里需要小心sprintf - 你不希望缓冲区溢出。
      • sprintf 以这种方式调用是否安全?为了使代码与贴出的python相媲美,您应该显示buf的初始化。
      • 当然会分配 buf[] 足够大。应该是 my_list 中数字数量的 11 倍。
      【解决方案7】:
      #include <stdio.h>
      #include <stdlib.h>
      
      /* My approach is to count the length of the string required. And do a single alloc.
           Sure you can allocate more, but I don't know for how long this data will be retained.
      */ 
      
      #define LEN(a) (sizeof a / sizeof *a)
      
      int main(void) {
      
          unsigned a[] = {1, 23, 45, 523, 544};
          int i, str_len=0, t_written=0;
          char tmp[11]; /* enough to fit the biggest unsigned int */
      
          for(i = 0; i < LEN(a); i++) 
              str_len += sprintf(tmp, "%d", a[i]);
      
          /* total: we need LEN(a) - 1 more for the ',' and + 1 for '\0' */
          str_len += LEN(a);
          char *str = malloc(str_len); 
          if (!str) 
              return 0;
      
          if (LEN(a) > 1) {
              t_written += sprintf(str+t_written, "%d", a[0]);
              for(i = 1; i < LEN(a); i++)
                  t_written += sprintf(str+t_written, ",%d", a[i]);
          } else if (LEN(a) == 1) 
              t_written += sprintf(str+t_written, "%d", a[0]);
      
          printf("%s\n", str);
      
          free(str);
          return 0;
      }
      

      【讨论】:

      • #define LEN(a) (sizeof a / sizeof *a)
      • 把这个主程序变成可复用的函数需要改多少?
      • 每次看到人们在int 类型中存储字符串或数组长度时,我都会感到畏缩。 size_t 有什么可怕的?
      • 冷静下来,克里斯。大家都知道sizeof返回size_t。对于这个演示,一个 int 就可以了。
      • 克里斯还有一件事,我检查了你的个人资料,跟踪了你的网站链接,只是为了找出像这样可怕的 C 代码:char ext[16], lang = malloc(16*sizeof(字符)); while(--start) { / 倒退,找到文件名的最后一个句点 */ sizeof(char) ?真的吗?哈哈。在使用它之前检查 lang 的返回值怎么样?格式是怎么回事?
      【解决方案8】:

      你们和你们不必要的特殊情况来处理尾随的逗号......销毁最后一个逗号比每次循环运行时进行条件检查更便宜。

      :)

      #include <stdio.h>
      
      char* toStr(int arr[], unsigned int arrSize, char buff[])
      {
          if (arr && arrSize && buff)
          {
              int* currInt = arr;
              char* currStr = buff;
              while (currInt < (arr + arrSize))
              {
                  currStr += sprintf(currStr, "%d,", *currInt++);
              }
              *--currStr = '\0';
          }
          return buff;
      }
      
      int main()
      {
          int arr[] = {1234, 421, -125, 15251, 15251, 52};
          char buff[1000];
      
          printf("Arr is:%s\n", toStr(arr, 6, buff));    
      }
      

      假设 buff 足够大,将其分配为 (最大 int 的长度 + 2) * arrSize)。启发了我的 memcpy :)

      编辑 我意识到我之前脑子有问题,可能只是增加了 sprintf 的返回值,而不是存储 temp。显然其他答案也这样做,编辑我的答案以删除 2 不必要的行。

      编辑2 看起来 wrang-wrang 打败了我!他的答案与我的几乎相同,并且之前已提交。我谦虚地建议给他+1。

      【讨论】:

      • *--currStr = '\0' 需要对任何必须在您之后维护代码的人进行解释性注释。 (i == 0 ? :) 没有。
      • 我想这是一个见仁见智的问题,但我 +1 是因为实际上对字符串 len 更安全。
      • 如果 arrSize == 0 这会中断。另外,您至少需要 s/size/arrSize/。
      • 还有一个问题:如果 arrSize 为 0,那么您将错过将 buff[0] 设置为 '\0'。
      【解决方案9】:

      假设当您提到“用于记录”时,您的意思是写入日志文件,那么您的解决方案可能看起来像这样(伪编码):

      for (int x in array) {
          fprintf(log, "%d", x);
          if (! last element)
              fputc(log, ',');
      }
      

      【讨论】:

        【解决方案10】:

        就个人而言,为了简单起见,也可能加快速度,我会分配一个大缓冲区,为每个元素的大小为“4,294,967,295”和“,”的数组留出空间。但是,在创建列表期间它的空间效率不高!

        然后我将 int 冲刺到那里,并将“,”附加到所有元素

        最后,我将重新分配指针以使其空间不超过所需空间。 (大小 = strlen)

        sprintf:成功时,返回写入的字符总数。此计数不包括在字符串末尾自动附加的额外空字符。

        这就是你如何跟踪 strcpy 在字符串中的位置。 :)

        希望对您有所帮助! :)

        如果您只想打印出来,请查看其他回复。 (for循环和printf)

        【讨论】:

        • 实际上,size = strlen-1,因为你不想要尾随的“,”。 :)
        【解决方案11】:

        不幸的是,总会有三种情况:

        • 空列表(无逗号,无项目)
        • 一项(无逗号,一项)
        • 两个或多个项目(n-1 个逗号,n 个项目)

        join 方法为您隐藏了这种复杂性,这就是它如此出色的原因。

        在 C 语言中,我会这样做:

        for (i = 0; i < len; i++)
        {
            if (i > 0)   /* You do need this separate check, unfortunately. */
                output(",");
            output(item[i]);
        }
        

        output 是您附加到字符串的位置。它可以像 strcat 在预分配的缓冲区上一样简单,也可以像 printf 到某个流一样简单(就像我今天在 Creating a FILE * stream that results in a string 中了解到的内存流:-)。

        如果您对每次都检查所有 i >= 1 感到恼火,您可以这样做:

        if (i > 0)
        {
            output(item[0]);
            for (i = 1; i < len; i++)
            {
                output(",");
                output(item[i]);
            }
        }
        

        【讨论】:

        • 使用 Duff 的设备(请参阅我的回答)了解如何避免检查每个循环和重复输出逻辑。
        【解决方案12】:

        如果你想要它到一个文件中,Steven Schlansker 的回答很好。

        但是,如果你想把它放在一个字符串中,事情就会变得更加复杂。您可以使用sprintf,但您需要注意不要用完字符串中的空间。如果您有 C99 兼容的 snprintf(Linux、BSD,而不是 Windows),则以下(未经测试、未编译)代码应该可以工作:

        char *buf = malloc(1024); /* start with 1024 chars */
        size_t len = 1024;
        int pos = 0;
        int rv;
        int i;
        for (i = 0; i < n; i++) {
            rv = snprintf(buf+pos, len - pos, "%s%d", i = 0 ? "" : ",", my_list[i]);
            if (rv < len - pos) {
                /* it fit */
                pos += rv;
            } else {
                len *= 2;
                buf = realloc(buf, len);
                if (!buf) abort();
                i--; /* decrement i to repeat the last iteration of the loop */
            }
        }
        return buf;
        

        调用者必须释放buf

        【讨论】:

        • 为什么要从1024开始,为什么要自己分配呢?允许调用者指定其缓冲区的大小,并由其负责。在你得到的范围内工作。
        • 同意。如果调用者释放,那么让调用者也分配是有意义的。
        【解决方案13】:
        void join(int arr[], int len, char* sep, char* result){
            if(len==0){
                *result='\0';
            } else {
                itoa(arr[0],result,10);
                if(len > 1){
                    strcat(result,sep);
                    join(arr+1,len-1,sep,result+strlen(result));
                }
            }
        }
        

        【讨论】:

          【解决方案14】:

          这是一个线性解决方案,它为调用者分配一个呈指数增长的缓冲区(对realloc 的调用更少,如果这很重要)。包含测试脚本。

          #include <stdbool.h>
          #include <stdio.h>
          #include <stdlib.h>
          #include <string.h>
          
          void ensure(bool pred, char *msg, char *file, int line) {
              if (!pred) {
                  fprintf(stderr, "%s:%d: %s", file, line, msg);
                  exit(1);
              }
          }
          
          char *arr_to_s(int len, int *arr, char *sep) {
              size_t sep_len = strlen(sep);
              int result_capacity = 16 + sep_len;
              int result_len = 0;
              char *result = malloc(result_capacity);
              ensure(result, "malloc", __FILE__, __LINE__);
              result[0] = '\0';
            
              for (int i = 0; i < len; i++) {
                  char num[16+sep_len];
                  int previous_len = result_len;
                  result_len += sprintf(num, i < len - 1 ? "%d%s" : "%d", arr[i], sep);
                
                  if (result_len >= result_capacity) {
                      result_capacity <<= 1;
                      result = realloc(result, result_capacity);
                      ensure(result, "realloc", __FILE__, __LINE__);
                  }
                
                  strcat(result + previous_len, num);
              }
            
              return result;
          }
          
          void run_basic_tests(void) {
              int tests[][4] = {
                  {0},
                  {0, 1},
                  {0, 1, 2},
                  {0, 42, 2147483647, -2147483648},
              };
              
              for (int i = 0; i < 4; i++) {
                  char *s = arr_to_s(i + 1, tests[i], ", ");
                  printf("[%s]\n", s);
                  free(s);
              }
          }
          
          void run_intensive_tests(int n) {
              srand(42);
          
              for (int i = 0; i < n; i++) {
                  int len = rand() % 2000;
                  int test[len];
          
                  printf("[");
          
                  for (int j = 0; j < len; j++) {
                      test[j] = rand() % 2000000000 - 1000000000;
                      printf(j < len - 1 ? "%d," : "%d", test[j]);
                  }
          
                  puts("]");
                  
                  char *s = arr_to_s(len, test, ",");
                  printf("[%s]\n", s);
                  free(s);
              }
          }
          
          int main(void) {
              //run_basic_tests();
              run_intensive_tests(10000);
              return 0;
          }
          

          测试运行器:

          #!/usr/bin/env bash
          
          gcc -std=c99 -pedantic -Wall \
              -Wno-missing-braces -Wextra -Wno-missing-field-initializers -Wformat=2 \
              -Wswitch-default -Wswitch-enum -Wcast-align -Wpointer-arith \
              -Wbad-function-cast -Wstrict-overflow=5 -Wstrict-prototypes -Winline \
              -Wundef -Wnested-externs -Wcast-qual -Wshadow -Wunreachable-code \
              -Wlogical-op -Wfloat-equal -Wstrict-aliasing=2 -Wredundant-decls \
              -Wold-style-definition -Werror \
              -ggdb3 \
              -O0 \
              -fno-omit-frame-pointer -ffloat-store -fno-common -fstrict-aliasing \
              -lm \
              -o arr_to_s.out \
              arr_to_s.c
          
          ./arr_to_s.out > arr_to_s_test_out.txt
          cat arr_to_s_test_out.txt | awk 'NR % 2 == 1' > arr_to_s_test_expected.txt
          cat arr_to_s_test_out.txt | awk 'NR % 2 == 0' > arr_to_s_test_actual.txt
          diff arr_to_s_test_expected.txt arr_to_s_test_actual.txt
          

          瓦尔格林:

          ==573== HEAP SUMMARY:
          ==573==     in use at exit: 0 bytes in 0 blocks
          ==573==   total heap usage: 103,340 allocs, 103,340 frees, 308,215,716 bytes allocated
          ==573==
          ==573== All heap blocks were freed -- no leaks are possible
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2018-08-16
            • 1970-01-01
            • 1970-01-01
            • 2021-12-18
            • 1970-01-01
            • 1970-01-01
            • 2021-12-06
            相关资源
            最近更新 更多