循环中的strcat()与sprintf()
我有一个程序,它删除字符串中的所有变量。这些变量以“$”开头。例如,如果我给出一个像[1,2,$1,$2]这样的字符串,它应该只返回[1,2] 但是,哪个循环对性能更有利 这: 或者这个:循环中的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
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);
}