strcat()vs sprintf()在循环中

时间:2018-05-18 17:03:30

标签: c performance loops printf strcat

我有一个程序可以删除字符串中的每个变量。这些变量以'$'开头。因此,举例来说,如果我给出一个像[1,2,$ 1,$ 2]这样的字符串,它应该只返回[1,2]。

然而,哪个循环更适合性能?

此:

while (token != NULL)
{
    if (*token != '$')
    {
        sprintf(dst, "%s,%s", dst, token);
    }
    token = strtok(NULL, "], ");
}

或者这个:

while (token != NULL)
{
    if (*token != '$')
    {
        strcat(dst, token);
        strcat(dst, ",");
    }
    token = strtok(NULL, "], ");
}

3 个答案:

答案 0 :(得分:6)

根据C11 standard 7.21.6.6 p2,描述sprintf

  

如果在重叠的对象之间进行复制,则行为是   未定义。

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

答案 1 :(得分:4)

  1. strtok具有破坏性,因此在执行此代码后输入字符串将无法使用。既然如此,你也可以进行转型。这有几个优点,其中一个优点是您不需要分配任何内存(因为最终字符串不能长于原始字符串)。这还需要一些额外的簿记,但这提供了另一个优点:所得到的函数的执行时间在输入的大小上是线性的。在每次迭代时从头开始重新启动输出缓冲区的扫描 - 正如两个解决方案所做的那样 - 在字符串的长度中使函数二次。 [注1]二次算法的使用比替代标准库调用的成本的微小差异要严重得多。

  2. 正如各种人所提到的,调用sprintf是未定义的行为,输出缓冲区与要打印的字符串之间存在重叠。因此,sprintf的使用不正确,即使它似乎可以用于某些实现。

  3. strcatsprintf都不会保护您免受缓冲区溢出的影响。您可以使用snprintf(通过将新字符串放在累积缓冲区的末尾,而不是在每次迭代时用自身覆盖缓冲区的开头),或者您可以使用strncat,但在这两种情况下你需要做一些额外的簿记。

  4. 所以这里是第一点提出的算法的快速实现。请注意,它不会调用malloc(),也不会在堆栈上分配任何字符串。另请注意,它使用memmove而不是memcpy在字符串中向前移动新发现的标记,以避免在令牌及其目标重叠时出现问题。 (memmove允许重叠; memcpystrcpystrcat不允许重叠。)

    /* 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;
    }
    

    API说明:

    • 与原始版本不同,它不会丢弃以$开头的令牌。这将是微不足道的补充。

    • 与原版不同,此功能会避免尾随,。同样,如果有充分的理由,这很容易改变。 (但是,尾随逗号表示只有一个标记的字符串最终会变成一个字符,因此无法进行就地保证。)

    • 我选择返回压缩字符串开头的地址(与输入缓冲区的地址相同),以与各种标准C接口保持一致。但是,在许多情况下,返回out(尾随NUL的地址)会更有用,以便允许进一步连接而无需计算字符串的新长度。或者,可以返回字符串的新长度,如sprintf所做的那样(return out - 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(token)的需要。

    注意:

    1. 从技术上讲,时间与字符串长度和令牌数量的乘积成正比,但在最坏的情况下(令牌很短),这实际上是O(n 2 )。

答案 2 :(得分:2)

这两种方法都不合适:

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

  • 带有两个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, "], ");
}