循环中的strcat()与sprintf()

循环中的strcat()与sprintf(),c,performance,loops,printf,strcat,C,Performance,Loops,Printf,Strcat,我有一个程序,它删除字符串中的所有变量。这些变量以“$”开头。例如,如果我给出一个像[1,2,$1,$2]这样的字符串,它应该只返回[1,2] 但是,哪个循环对性能更有利 这: 或者这个: while (token != NULL) { if (*token != '$') { strcat(dst, token); strcat(dst, ","); } token = strtok(NULL, "], "); } 根据,描述s

我有一个程序,它删除字符串中的所有变量。这些变量以“$”开头。例如,如果我给出一个像[1,2,$1,$2]这样的字符串,它应该只返回[1,2]

但是,哪个循环对性能更有利

这:

或者这个:

while (token != NULL)
{
    if (*token != '$')
    {
        strcat(dst, token);
        strcat(dst, ",");
    }
    token = strtok(NULL, "], ");
}
根据,描述sprintf:

如果复制发生在重叠的对象之间,则行为为 未定义


因此,您的第一个代码段在从
dst
复制到
dst
时调用了一个未定义的行为。
strcat方法没有这个问题。

两种方法都不可取:

  • 将与
    %s
    格式说明符的目标和源相同的指针传递给
    sprintf
    具有未定义的行为。此外,如果目标阵列不够大,
    sprintf
    无法防止缓冲区溢出

  • 第二种方法是调用两个strcat
    strcat
    调用,可能会出现缓冲区溢出问题,并且效率低下,因为重复扫描目标字符串以找到副本的位置

以下是另一种方法:

char src[LINE_SIZE];
char dst[LINE_SIZE + 1];  /* dst is large enough for the copy */
int pos = 0;

token = strtok(src, "], ");
while (token != NULL) {
    if (*token != '$') {
        pos += sprintf(dst + pos, "%s,", token);
    }
    token = strtok(NULL, "], ");
}
  • strtok
    具有破坏性,因此执行此代码后,输入字符串将不可用。在这种情况下,您最好就地进行转换。这有两个优点,其中之一是不需要分配任何内存(因为最后一个字符串不能比原始字符串长)。这也需要一点额外的簿记,但这提供了另一个优势:结果函数的执行时间与输入的大小成线性关系。在每次迭代中从一开始就重新启动输出缓冲区的扫描—就像两个解决方案一样—使函数在字符串长度上呈二次型。[注1]二次算法的使用比替代标准库调用成本的微小差异要严重得多

  • 正如许多人所提到的,调用
    sprintf
    的方式使输出缓冲区和要打印的一个字符串之间存在重叠,这是一种未定义的行为。因此,您使用的
    sprintf
    是不正确的,尽管它可能在某些实现中起作用

  • 无论是
    strcat
    还是
    sprintf
    都不能防止缓冲区溢出。您可以使用
    snprintf
    (通过将新字符串放在累积缓冲区的末尾,而不是在每次迭代时覆盖缓冲区的开头)或者您可以使用
    strncat
    ,但在这两种情况下,您都需要做一些额外的簿记

  • 下面是第一点中提出的算法的快速实现。请注意,它既不调用
    malloc()
    ,也不在堆栈上分配任何字符串。还请注意,它使用
    memmove
    而不是
    memcpy
    在字符串中向前移动新发现的令牌,以避免令牌与其目标重叠时出现问题。(
    memmove
    允许重叠;
    memcpy
    strcpy
    strcat
    不允许重叠。)

    API注释:
    • 与原始标记不同,这不会丢弃以
      $
      开头的标记。这将是微不足道的补充

    • 与原始函数不同的是,此函数还避免了尾随的
      。同样,如果有充分的理由,这一点很容易改变。(但是,后面的逗号意味着只有一个标记的字符串最终会长一个字符,因此无法进行就地保证。)

    • 我选择返回压缩字符串的起始地址(与输入缓冲区的地址相同),以与各种标准C接口保持一致。但是,在许多情况下,返回
      out
      (这是尾随NUL的地址)更有用,以便在不计算字符串新长度的情况下实现进一步的连接。或者,可以返回字符串的新长度,就像
      sprintf
      那样(
      returnout-str;

    • 这个API诚实地表明它破坏了原始字符串(通过用转换后的字符串覆盖它);函数在其输入上只调用strtok,但返回单独的输出,这可能会导致微妙的错误,因为调用者不清楚原始字符串是否已被破坏。虽然在调用
      strtok
      后无法恢复字符串,但只需复制原始字符串,即可将就地算法转换为非破坏性算法:

      /* Returns freshly allocated memory; caller is responsible for freeing it */
      char* compress_and_copy(const char* str, const char* fs, char ofs) {
        return compress(strdup(str), fs, ofs);
      }
      
    • 当然,有可能原始的未简化函数没有保证生成较短字符串的功能;例如,它可以通过用变量值替换以
      $
      开头的段来扩展这些段。在这种情况下,需要生成一个新字符串

      在某些情况下,甚至在大多数情况下,输出仍然比输入短。但是,如果可能的话,应该抵制在适当的位置进行转换的诱惑,并且仅在必要时分配一个新字符串。虽然它可能更有效,但它使分配所有权的规则复杂化;您最终不得不说“调用者只有在返回的字符串与原始字符串不同时才拥有该字符串”,这既笨拙又容易发生意外

      因此,如果这是实际用例,最佳解决方案(从干净API设计的角度来看)是使用
      strspn()
      strcspn()
      以非破坏性方式遍历原始字符串。这是一个多一点的工作,因为它需要更多的簿记;另一方面,它避免了在标识令牌后重新计算strlen(令牌)

    笔记:
  • /* Compresses the string str in place by removing leading and trailing separator
     * characters (which are the characters in 'fs') and replacing any interior
     * sequence of separator characters with a single instance of 'ofs'.
     * Returns 'str'.
     */
    char* compress(char* str, const char* fs, char ofs) {
      char* out = str;
      char* token = strtok(str, fs);
      while (token != NULL) {
        size_t tlen = strlen(token);
        memmove(out, token, tlen);
        out += tlen;
        *out++ = ofs;
        token = strtok(NULL, fs);
      }
      /* Overwrite the last delimiter (if there was one) with a NUL */
      if (out != str) --out;
      *out = 0;
      return str;
    }
    
    /* Returns freshly allocated memory; caller is responsible for freeing it */
    char* compress_and_copy(const char* str, const char* fs, char ofs) {
      return compress(strdup(str), fs, ofs);
    }