计算字符串长度的不同方法

3

我在我的一个回答中收到了评论,让我有些困惑。当尝试计算将两个字符串连接到新的内存块所需的内存量时,有人说使用snprintfstrlen更好,如下所示:

size_t length = snprintf(0, 0, "%s%s", str1, str2);
// preferred over:
size_t length = strlen(str1) + strlen(str2);

我能否了解一下这背后的原因?如果有的话,有什么优势,而且是否会看到一个结果与另一个结果不同?

2
为什么不直接问评论者呢? - Hans Passant
7个回答

5

我是说这句话的人,在我的评论中粗心地快速写下,遗漏了+1,所以让我解释一下。我的意思只是你应该使用相同的方法来计算最终用于填充字符串的长度,而不是使用两种可能会有微妙差异的不同方法。

例如,如果你有三个字符串而不是两个,并且其中两个或更多重叠,那么strlen(str1)+strlen(str2)+strlen(str3)+1可能超过SIZE_MAX并回绕到零,导致输出被截断(如果使用snprintf)或极其危险的内存损坏(如果使用strcpystrcat)。

snprintf将在生成的字符串长度大于INT_MAX时返回-1,并设置errno=EOVERFLOW,因此您受到保护。但是,在使用之前需要检查返回值,并添加一个空终止符。


3
如果您只需要确定两个字符串连接后的大小,我认为没有任何特殊原因优先选择snprintf,因为两个strlen调用可以完成确定两个字符串总长度的最小操作。snprintf几乎肯定会更慢,因为它除了遍历两个字符串计算字符数之外,还必须检查参数并解析格式字符串。
但是,如果您处于一种情况下,想要将两个字符串连接起来,并且有一个静态的、不太大的缓冲区来处理正常情况,但在处理大字符串时可以回退到动态分配的缓冲区,那么使用snprintf可能是一个明智的选择,例如:
/* static buffer "big enough" for most cases */
char buffer[256];
/* pointer used in the part where work on the string is actually done */
char * outputStr=buffer;
/* try to concatenate, get the length of the resulting string */
int length = snprintf(buffer, sizeof(buffer), "%s%s", str1, str2);
if(length<0)
{
    /* error, panic and death */
}
else if(length>sizeof(buffer)-1)
{
    /* buffer wasn't enough, allocate dynamically */
    outputStr=malloc(length+1);
    if(outputStr==NULL)
    {
        /* allocation error, death and panic */
    }
    if(snprintf(outputStr, length, "%s%s", str1, str2)<0)
    {
        /* error, the world is doomed */
    }
}

/* here do whatever you want with outputStr */

if(outputStr!=buffer)
    free(outputStr);

0

我认为这里的“优点”是,strlen(NULL) 可能会导致段错误,而(至少glibc的)snprintf()可以在不失败的情况下处理 NULL参数。

因此,使用glibc-snprintf()时,您不需要检查字符串之一是否为NULL,尽管length可能会比实际需要稍微大一些,因为(至少在我的系统上)printf("%s", NULL); 会打印“(null)”而非空字符串。


然而,我不建议使用snprintf()代替strlen()。这不够明显。一个更好的解决方案是编写一个strlen()的包装器,在参数是NULL时返回0:

size_t my_strlen(const char *str)
{
    return str ? strlen(str) : 0;
}

snprintf 不接受 %s 格式说明符参数的 NULL 值。 - R.. GitHub STOP HELPING ICE
R..:这又是 GNU 的一个愚蠢扩展吗?因为我的 glibc-snprintf() 非常乐意接受 %s 格式说明符的 NULL 参数。 - Philip
这是UB,因此实现有权进行任何操作。在这种情况下,glibc的行为就像参数是“(null)”一样。 - R.. GitHub STOP HELPING ICE
R..:感谢您指出这一点。我已经更新了我的答案,不过现在我可能也会将其删除... - Philip
请不要删除,因为这个讨论对解决问题非常有帮助。请注意,您的my_strlen包装器将返回0用于“NULL”,而glibc的snprintf将输出6个字节用于“NULL”。这可能会导致字符串后面的部分被截断,或者如果有人愚蠢到只使用sprintf并假设正确的长度已被计算,则会导致缓冲区溢出。 - R.. GitHub STOP HELPING ICE

0

一个优点是输入字符串只被扫描了一次(在 snprintf() 中),而不是两次用于 strlen/strcpy 解决方案。

实际上,在重新阅读这个问题和你之前回答的评论后,我没有看出使用 sprintf() 来仅仅计算连接字符串长度的意义所在。如果你实际上正在进行连接操作,那么我的上面一段话就适用了。


4
@Pete Wilson:你是错误的。根据我系统上的 man snprintf 文档:"当以 size=0 调用 snprintf() 时... C99 允许在这种情况下 str 参数为 NULL,并像往常一样返回已经写入的字符数,假设输出字符串足够大。" - Greg Hewgill

0

编辑:已删除随机、错误的无意义内容。我说过那个吗?

编辑:在下面评论中,Matteo是绝对正确的,而我是绝对错误的。

来自C99:

2 snprintf函数等同于fprintf,不同之处在于输出被写入一个数组(由参数s指定),而不是流中。如果n为零,则不写入任何内容,s可以是空指针。否则,超出第n-1个字符的输出将被丢弃,而不是写入数组,并在实际写入数组的字符末尾写入一个空字符。如果在重叠对象之间进行复制,则行为未定义。

返回值:

3 snprintf函数返回将被写入的字符数,假设n足够大,不包括终止的空字符,或者如果发生编码错误则返回负值。因此,仅当返回值为非负且小于n时,才完全写入以空字符结尾的输出。

谢谢你,Matteo,我向OP道歉。

这是个好消息,因为它给了我一个我三周前在这里提出的问题的积极答案。我无法解释为什么我没有阅读所有的回答,而那些回答给了我想要的东西。太棒了!


2
snprintf手册中可以看到:“关于snprintf()的返回值,SUSv2和C99相互矛盾:当size=0时,SUSv2规定返回值为小于1的未指定值,而C99允许在这种情况下str为空,并且(像往常一样)返回值是输出字符串足够大时将要写入的字符数。” 因此,在两种情况下都不会出现核心转储,并且只要您的编译器符合C99标准,该调用就是定义良好的。 - Matteo Italia
从C99标准(§7.19.6.5 ¶2)中可以看出:“如果n为零,则不会写入任何内容,且s可以是空指针。”(以及¶3)“snprintf函数返回将被写入的字符数,假设n足够大,但不包括终止的空字符。” - Matteo Italia
@M,谢谢,现在我会在另一个问题上给自己投反对票,因为我没有阅读所有答案 :-)(心想:我才不会呢 :-)) - Pete Wilson

0
你需要在strlen()示例中加1。记住,你需要为NUL终止字节分配空间。

snprintf 表达式中加 1 也是必要的,因为它不包含在返回值中。出于简单起见,我只是从两个中省略了它。 - user142162

0

所以snprintf()给了我一个字符串的大小,这意味着我可以为它分配空间。非常有用。

我一直想要(但直到现在才找到)snprintf()的这个函数,因为我会为输出格式化大量的字符串;但我不想为输出分配静态缓冲区,因为很难预测输出的长度。所以我最终得到了很多长度为4096的字符数组 :-(

但现在 - 使用这个新发现的(对我来说)snprintf()计数函数,我可以malloc()输出缓冲区并且晚上也能睡得安稳。

再次感谢OP和Matteo。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接