这些从 fgets 中删除 '\n' 的方法在性能方面是否有所不同?
Does these methods of removing '\n' left from fgets differ concerning performance?
我正在学C,如果这道题看起来很简单或者是新手题,那你就知道为什么了。
所以,我知道有很多方法可以删除 fgets()
留下的 '\n'
,正如已经在 SO [=21= 上讨论的那样].
我将重点讨论这三种方法:
char *strchr(const char *s, int c);
if (fgets(sentence, 11, stdin) != NULL) {
p = strchr(sentence, '\n');
*p = '[=10=]';
}
char *strtok(char *str, const char *delim);
if (fgets(sentence, 11, stdin) != NULL)
token = strtok(sentence, "\n");
size_t strcspn(const char *s, const char *reject);
if (fgets(sentence, 11, stdin) != NULL)
sentence[strcspn(sentence, "\n")] = '[=12=]';
假设变量p
和token
声明为char *p = NULL, *token = NULL;
他们各司其职,但就性能而言,他们有什么不同吗?
有一次在网上冲浪(很抱歉我没有证据,因为我忘记了link)我发现strspn
并不是一个很好的方法如果有人对性能感兴趣,那就去做吧,因此我的问题。
在 SO 上发布此内容之前,我已经在此处进行了搜索,但没有找到我想知道的内容。我也尝试过自己分析,都使用在 SO 上找到的 time ./executable
和 this 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 > 0
和 sentence[len - 1] == '\n'
使用 2 个额外测试,降低性能。
请注意,某些库使用 compile-time 测试,可能允许对 1 字节字符串文字参数进行特殊封装。在这种情况下,编译器可以生成比 strchr
.
更高效的内联代码
确实 strcspn
可能在一些 C 库中以相当幼稚的方式实现,但 GNU libc 和 Apple C 库都不是这种情况。它们的实现非常高效,至少 gcc
使用此函数和其他字符串函数的内置知识来生成更好的代码。
在优化实施 strchrnul
的环境中,这种方法应该很难被击败。
一如既往,运行 使用不同的编译器、处理器、C 库对不同的备选方案进行基准测试和分析...不同的平台可能会产生截然不同的结果。
我正在学C,如果这道题看起来很简单或者是新手题,那你就知道为什么了。
所以,我知道有很多方法可以删除 fgets()
留下的 '\n'
,正如已经在 SO [=21= 上讨论的那样].
我将重点讨论这三种方法:
char *strchr(const char *s, int c);
if (fgets(sentence, 11, stdin) != NULL) { p = strchr(sentence, '\n'); *p = '[=10=]'; }
char *strtok(char *str, const char *delim);
if (fgets(sentence, 11, stdin) != NULL) token = strtok(sentence, "\n");
size_t strcspn(const char *s, const char *reject);
if (fgets(sentence, 11, stdin) != NULL) sentence[strcspn(sentence, "\n")] = '[=12=]';
假设变量p
和token
声明为char *p = NULL, *token = NULL;
他们各司其职,但就性能而言,他们有什么不同吗?
有一次在网上冲浪(很抱歉我没有证据,因为我忘记了link)我发现strspn
并不是一个很好的方法如果有人对性能感兴趣,那就去做吧,因此我的问题。
在 SO 上发布此内容之前,我已经在此处进行了搜索,但没有找到我想知道的内容。我也尝试过自己分析,都使用在 SO 上找到的 time ./executable
和 this 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 > 0
和 sentence[len - 1] == '\n'
使用 2 个额外测试,降低性能。
请注意,某些库使用 compile-time 测试,可能允许对 1 字节字符串文字参数进行特殊封装。在这种情况下,编译器可以生成比 strchr
.
确实 strcspn
可能在一些 C 库中以相当幼稚的方式实现,但 GNU libc 和 Apple C 库都不是这种情况。它们的实现非常高效,至少 gcc
使用此函数和其他字符串函数的内置知识来生成更好的代码。
在优化实施 strchrnul
的环境中,这种方法应该很难被击败。
一如既往,运行 使用不同的编译器、处理器、C 库对不同的备选方案进行基准测试和分析...不同的平台可能会产生截然不同的结果。