这些从 fgets 中删除 '\n' 的方法在性能方面是否有所不同?

Does these methods of removing '\n' left from fgets differ concerning performance?

我正在学C,如果这道题看起来很简单或者是新手题,那你就知道为什么了。

所以,我知道有很多方法可以删除 fgets() 留下的 '\n',正如已经在 SO [=21= 上讨论的那样].

我将重点讨论这三种方法:

假设变量ptoken声明为char *p = NULL, *token = NULL;

他们各司其职,但就性能而言,他们有什么不同吗?

有一次在网上冲浪(很抱歉我没有证据,因为我忘记了link)我发现strspn并不是一个很好的方法如果有人对性能感兴趣,那就去做吧,因此我的问题。

在 SO 上发布此内容之前,我已经在此处进行了搜索,但没有找到我想知道的内容。我也尝试过自己分析,都使用在 SO 上找到的 time ./executablethis method。但是我没有运气,因为结果不一致。

谁能帮我看看我的描述是错误的还是它们真的相等?

编辑: Here 是 link 我发现 strcspn 效率不高的地方。

写的这个方法

if (fgets(sentence, 11, stdin) != NULL) {
    p = strchr(sentence,'\n');
    p = '[=10=]';
    //^^ must be *p 
}

不正确,因为字符串中可能没有换行符。在这种情况下,指针 p 将等于 NULL 并且代码片段将具有未定义的行为。

你需要改成这样

    if ( p ) *p = '[=11=]';

    if ( ( p = strchr(sentence,'\n') ) != NULL ) *p = '[=12=]';

因为只搜索了一个字符,所以效率很高。

但是它的缺点是您需要一个额外的变量来指向换行符。

这个方法

if (fgets(sentence, 11, stdin) != NULL)
    token = strtok(sentence, "\n");

语义上不是很合适。当您需要将字符串拆分为标记时,通常在其他上下文中使用函数 strtok。 如果字符串只包含换行符,函数 returns 一个空指针。

所以最合适的方法是这样

if (fgets(sentence, 11, stdin) != NULL)
    sentence[strcspn(sentence, "\n")] = 0;

因为它是安全的,不需要额外的变量。

至于我,那么在 C++ 中,我会使用

if ( char *p = strchr( sentence, '\n' ) ) *p = '[=15=]';

在 C 中,我会使用 :)

sentence[strcspn(sentence, "\n")] = '[=16=]';

在我们讨论性能之前,验证正确性很重要。

让我们看看您的方法,以及其他一些流行的方法:

strchr

if (fgets(sentence, 11, stdin) != NULL) {
    p = strchr(sentence, '\n');
    *p = '[=10=]';
}

您忘记测试 p 是否不是 NULL。这是一个大问题,因为 sentence 可能不包含 \n,要么是因为读取的行太长并且只有一部分在 sentence 中,要么是最后一行在文件中,如果它不是由 \n 终止,或者如果文件包含空字节。你应该这样写这个版本:

if (fgets(sentence, 11, stdin) != NULL) {
    char *p = strchr(sentence, '\n');
    if (p != NULL)
        *p = '[=11=]';
    ...
}

strchrnul

一些 C 库有一个非标准函数 strchrnul 带有这个原型:

char *strchrnul(const char *s, int c);

它 return 是指向字符串 s 中第一次出现的 c 的指针,如果找不到,则指向最后一个 [=27=] 的指针。这个函数允许一种非常简单有效的方法来剥离 \n:

if (fgets(sentence, 11, stdin) != NULL) {
    *strchrnul(sentence, '\n') = '[=13=]';
    ...
}

唯一的缺点是此函数不是 C 标准的一部分,在某些平台上可能不可用。

strtok

if (fgets(sentence, 11, stdin) != NULL) {
    token = strtok(sentence, "\n");
    ...
}

此版本不正确:strtok 对其内部数据有副作用。此版本将干扰使用 strtok 的周围代码。如果将此方法隐藏在函数中,则会隐藏此副作用,并可能导致使用您的函数的程序员难以发现错误。您可以使用 strtok 的可重入版本:strtok_r,但它并不总是可用。

此外,正如 user3121023 评论的那样,strtok 不会删除 \n 如果它位于字符串的开头。这绝对不符合这种方法。 (strtok 怪癖太多,无论如何应该完全避免。)

strlen

您没有提到 strlen 替代方案。我经常看到它是这样写的:

if (fgets(sentence, 11, stdin) != NULL) {
    sentence[strlen(sentence) - 1] = '[=15=]';
    ...
}

由于多种原因,这是不正确的:

  • sentence 可能没有 \n 作为它的最后一个字符,正如 strchr 版本已经解释过的那样。尝试以这种方式删除 \n 会删除有效字符。

  • sentence 可能是一个空字符串,在这种情况下代码将具有未定义的行为。 sentence 为空需要 C 标准中未指定的特殊条件:如果输入流在一行的开头包含 NUL 字节,fgets() 可能 return 一个空缓冲区。

为了正确性,这个方法应该这样实现:

if (fgets(sentence, 11, stdin) != NULL) {
    size_t len = strlen(sentence);
    if (len > 0 && sentence[len - 1] == '\n')
        sentence[--len] = '[=16=]';
    // useful side effect: len has been updated.
    ...
}

strcspn

if (fgets(sentence, 11, stdin) != NULL) {
    sentence[strcspn(sentence, "\n")] = '[=17=]';
    ...
}

这是最简单的版本。无论 sentence 是否包含 \n 甚至对于空字符串,它都有效。它不太可能被程序员滥用。

性能

strcspn 是否比其他的效率更高或更低在很大程度上取决于 C 库实现和编译器性能。性能应该优于 strtok,因为它只进行一次扫描。它的效率可能低于 strchr,甚至低于 strlen,但为了正确性,strlen 替代方案还应该对 len > 0sentence[len - 1] == '\n' 使用 2 个额外测试,降低性能。

请注意,某些库使用 compile-time 测试,可能允许对 1 字节字符串文字参数进行特殊封装。在这种情况下,编译器可以生成比 strchr.

更高效的内联代码

确实 strcspn 可能在一些 C 库中以相当幼稚的方式实现,但 GNU libc 和 Apple C 库都不是这种情况。它们的实现非常高效,至少 gcc 使用此函数和其他字符串函数的内置知识来生成更好的代码。

在优化实施 strchrnul 的环境中,这种方法应该很难被击败。

一如既往,运行 使用不同的编译器、处理器、C 库对不同的备选方案进行基准测试和分析...不同的平台可能会产生截然不同的结果。