我有一个程序可以删除字符串中的每个变量。这些变量以'$'开头。因此,举例来说,如果我给出一个像[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, "], ");
}
答案 0 :(得分:6)
根据C11 standard 7.21.6.6 p2,描述sprintf
:
如果在重叠的对象之间进行复制,则行为是 未定义。
因此,当您从dst
复制到dst
时,您的第一个代码段会调用未定义的行为。 strcat
方法没有此问题。
答案 1 :(得分:4)
strtok
具有破坏性,因此在执行此代码后输入字符串将无法使用。既然如此,你也可以进行转型。这有几个优点,其中一个优点是您不需要分配任何内存(因为最终字符串不能长于原始字符串)。这还需要一些额外的簿记,但这提供了另一个优点:所得到的函数的执行时间在输入的大小上是线性的。在每次迭代时从头开始重新启动输出缓冲区的扫描 - 正如两个解决方案所做的那样 - 在字符串的长度中使函数二次。 [注1]二次算法的使用比替代标准库调用的成本的微小差异要严重得多。
正如各种人所提到的,调用sprintf
是未定义的行为,输出缓冲区与要打印的字符串之间存在重叠。因此,sprintf
的使用不正确,即使它似乎可以用于某些实现。
strcat
和sprintf
都不会保护您免受缓冲区溢出的影响。您可以使用snprintf
(通过将新字符串放在累积缓冲区的末尾,而不是在每次迭代时用自身覆盖缓冲区的开头),或者您可以使用strncat
,但在这两种情况下你需要做一些额外的簿记。
所以这里是第一点提出的算法的快速实现。请注意,它不会调用malloc()
,也不会在堆栈上分配任何字符串。另请注意,它使用memmove
而不是memcpy
在字符串中向前移动新发现的标记,以避免在令牌及其目标重叠时出现问题。 (memmove
允许重叠; memcpy
,strcpy
和strcat
不允许重叠。)
/* 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;
}
与原始版本不同,它不会丢弃以$
开头的令牌。这将是微不足道的补充。
与原版不同,此功能会避免尾随,
。同样,如果有充分的理由,这很容易改变。 (但是,尾随逗号表示只有一个标记的字符串最终会变成一个字符,因此无法进行就地保证。)
我选择返回压缩字符串开头的地址(与输入缓冲区的地址相同),以与各种标准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)
的需要。
答案 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, "], ");
}