Strtok 奇怪的行为
Strtok strange behaviour
我在使用 strtok 函数时遇到了一些问题。
作为练习,我必须通过排除空格、将首字母转换为大写字母并在一行中打印不超过 20 个字符来处理文本文件。
这是我的代码片段:
fgets(sentence, SIZE, f1_ptr);
char *tok_ptr = strtok(sentence, " \n"); //tokenazing each line read
tok_ptr[0] = toupper(tok_ptr[0]); //initials to capital letters
int num = 0, i;
while (!feof(f1_ptr)) {
while (tok_ptr != NULL) {
for (i = num; i < strlen(tok_ptr) + num; i++) {
if (i % 20 == 0 && i != 0) //maximum of 20 char per line
fputc('\n', stdout);
fputc(tok_ptr[i - num], stdout);
}
num = i;
tok_ptr = strtok(NULL, " \n");
if (tok_ptr != NULL)
tok_ptr[0] = toupper(tok_ptr[0]);
}
fgets(sentence, SIZE + 1, f1_ptr);
tok_ptr = strtok(sentence, " \n");
if (tok_ptr != NULL)
tok_ptr[0] = toupper(tok_ptr[0]);
}
文本只是一堆行,我只是作为参考显示:
Watch your thoughts ; they become words .
Watch your words ; they become actions .
Watch your actions ; they become habits .
Watch your habits ; they become character .
Watch your character ; it becomes your destiny .
这是我最后得到的:
WatchYourThoughts;Th
eyBecomeWords.WatchY
ourWords;THeyBecomeA
ctions.WatchYourActi
ons;TheyBecomeHabits
.WatchYourHabits;The
yBecomeCharacteR.Wat
chYourCharacter;ItBe
comesYourDEstiny.Lao
-Tze
最终结果大部分是正确的,但有时(例如 中的“他们”变成了(并且仅在这种情况下)或“命运”)单词未正确标记.因此,例如,在我进行的操作之后,“他们”被分成“t”和“嘿”,导致 THey(另一个实例中的 DEstiny)。
是一些错误还是我遗漏了什么?可能我的代码效率不高,某些条件可能最终变得很关键...
谢谢你的帮助,没什么大不了的,我只是不明白为什么会发生这种行为。
从专业的角度来看,这实际上是一个比某些评论可能暗示的更有趣的 OP,尽管问题的 'newcomer' 方面有时可能会引发相当深刻的、被低估的问题。
有趣的是,在我的平台(W10、MSYS2、gcc v.10.2)上,您的代码运行良好,结果正确:
WatchYourThoughts;Th
eyBecomeWords.WatchY
ourWords;TheyBecomeA
ctions.WatchYourActi
ons;TheyBecomeHabits
.WatchYourHabits;The
yBecomeCharacter.Wat
chYourCharacter;ItBe
comesYourDestiny.
那么首先,恭喜新人:你的编码还不错。
这表明不同的编译器可能会或可能不会防止有限的不当编码或规范滥用,可能会或可能不会保护堆栈或堆。
这就是说,@Andrew Henle 的评论指向一个关于 feof
的启发性答案是非常相关的。
如果您遵循它并检索您的 feof
测试,只需将其向下移动 after 读取检查,not before(如下所示)。您的代码应该会产生更好的结果(注意:我只会对您的代码进行最少的改动,故意忽略较小的问题):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <ctype.h>
#define SIZE 100 // add some leeway to avoid off-by-one issues
int main()
{
FILE* f1_ptr = fopen("C:\Users\Public\Dev\test_strtok", "r");
if (! f1_ptr)
{
perror("Open issue");
exit(EXIT_FAILURE);
}
char sentence[SIZE] = {0};
if (NULL == fgets(sentence, SIZE, f1_ptr))
{
perror("fgets issue"); // implementation-dependent
exit(EXIT_FAILURE);
}
errno = 0;
char *tok_ptr = strtok(sentence, " \n"); //tokenizing each line read
if (tok_ptr == NULL || errno)
{
perror("first strtok parse issue");
exit(EXIT_FAILURE);
}
tok_ptr[0] = toupper(tok_ptr[0]); //initials to capital letters
int num = 0;
size_t i = 0;
while (1) {
while (1) {
for (i = num; i < strlen(tok_ptr) + num; i++) {
if (i % 20 == 0 && i != 0) //maximum of 20 char per line
fputc('\n', stdout);
fputc(tok_ptr[i - num], stdout);
}
num = i;
tok_ptr = strtok(NULL, " \n");
if (tok_ptr == NULL) break;
tok_ptr[0] = toupper(tok_ptr[0]);
}
if (NULL == fgets(sentence, SIZE, f1_ptr)) // let's get away whith annoying +1,
// we have enough headroom
{
if (feof(f1_ptr))
{
fprintf(stderr, "\n%s\n", "Found EOF");
break;
}
else
{
perror("Unexpected fgets issue in loop"); // implementation-dependent
exit(EXIT_FAILURE);
}
}
errno = 0;
tok_ptr = strtok(sentence, " \n");
if (tok_ptr == NULL)
{
if (errno)
{
perror("strtok issue in loop");
exit(EXIT_FAILURE);
}
break;
}
tok_ptr[0] = toupper(tok_ptr[0]);
}
return 0;
}
$ ./test
WatchYourThoughts;Th
eyBecomeWords.WatchY
ourWords;TheyBecomeA
ctions.WatchYourActi
ons;TheyBecomeHabits
.WatchYourHabits;The
yBecomeCharacter.Wat
chYourCharacter;ItBe
comesYourDestiny.
Found EOF
您的代码中有大量错误,over-complicating 问题出在您身上。最紧迫的错误是Why is while ( !feof (file) ) always wrong? 为什么?在循环中跟踪 execution-path。您尝试使用 fgets()
进行阅读,然后您使用 sentence
而不知道是否已到达 EOF
调用 tok_ptr = strtok(sentence, " \n");
,然后再开始检查 feof(f1_ptr)
当你真正达到 EOF
时会发生什么?那是 “为什么 while ( !feof (file) ) 总是错误的?” 相反,你总是想用读取的 return 来控制你的 read-loop您正在使用的功能,例如while (fgets(sentence, SIZE, f1_ptr) != NULL)
您实际需要代码做什么?
更大的问题是,为什么您 over-complicating 是 strtok
和数组(以及 fgets()
的问题)?想想你需要做什么:
- 读取文件中的每个字符,
- 如果是空格,忽略它,设置in-word标志
false
,
- if a non-whitespace,if word中的第一个字符,将其大写,输出该字符,设置in-word标志
true
并增加输出到当前行的字符数, 最后
- 如果是第20个字符输出,则输出一个换行符并将计数器清零。
您的 C-toolbox 需要的 bare-minimum 工具是 fgetc()
、isspace()
和 toupper()
来自 ctype.h
,一个计数器输出的字符数,以及一个标志,用于判断该字符是否是空格后的第一个 non-whitespace 字符。
实现逻辑
这样问题就很简单了。读取一个字符,是空格吗,设置你的in-word标志false
,否则如果你的in-word标志是false
,将它大写,输出字符,设置你的in-word flag true
,增加你的字数。您需要做的最后一件事是检查您的 character-count 是否已达到限制,如果是,则输出 '\n'
并将您的 character-count 重置为零。重复,直到 运行 个字符不足。
您可以将其转换为类似于以下内容的代码:
#include <stdio.h>
#include <ctype.h>
#define CPL 20 /* chars per-line, if you need a constant, #define one (or more) */
int main (int argc, char **argv) {
int c, in = 0, n = 0; /* char, in-word flag, no. of chars output in line */
/* use filename provided as 1st argument (stdin by default) */
FILE *fp = argc > 1 ? fopen (argv[1], "r") : stdin;
if (!fp) { /* validate file open for reading */
perror ("file open failed");
return 1;
}
while ((c = fgetc(fp)) != EOF) { /* read / validate each char in file */
if (isspace(c)) /* char is whitespace? */
in = 0; /* set in-word flag false */
else { /* otherwise, not whitespace */
putchar (in ? c : toupper(c)); /* output char, capitalize 1st in word */
in = 1; /* set in-word flag true */
n++; /* increment character count */
}
if (n == CPL) { /* CPL limit reached? */
putchar ('\n'); /* output newline */
n = 0; /* reset cpl counter */
}
}
putchar ('\n'); /* tidy up with newline */
if (fp != stdin) /* close file if not stdin */
fclose (fp);
}
例子Use/Output
鉴于你的输入文件存储在我的计算机上 dat/text220.txt
,你可以生成你正在寻找的输出:
$ ./bin/text220 dat/text220.txt
WatchYourThoughts;Th
eyBecomeWords.WatchY
ourWords;TheyBecomeA
ctions.WatchYourActi
ons;TheyBecomeHabits
.WatchYourHabits;The
yBecomeCharacter.Wat
chYourCharacter;ItBe
comesYourDestiny.
(代码的可执行文件被编译为 bin/text220
,我通常为数据、目标文件和可执行文件保留单独的 dat
、obj
和 bin
目录保持源代码目录干净)
注意: 默认从 stdin
读取 如果没有文件名作为程序的第一个参数提供,您可以使用您的程序直接读取输入,例如
$ echo "my dog has fleas - bummer!" | ./bin/text220
MyDogHasFleas-Bummer
!
不需要花哨的字符串函数,只需一个循环、一个字符、一个标志和一个计数器 -- 其余的只是算术运算。总是值得尝试将您的编程问题归结为基本步骤,然后环顾您的 C-toolbox 并为每个基本步骤找到合适的工具。
使用strtok
不要误会我的意思,使用 strtok
没有任何问题,在这种情况下它是一个相当简单的解决方案——我的意思是对于简单的 character-oriented string-processing,它通常只是简单地循环遍历行中的字符。使用数组 fgets()
和 strtok()
不会获得任何效率,从文件中读取的内容已经放入 BUFSIZ
1[=129= 的缓冲区中].
如果你确实想使用 strtok()
,你应该用 fgets()
中的 return 控制你 read-loop 然后你可以用 [=41= 标记化] 还在每个点检查其 return。带有 fgets()
的 read-loop 和带有 strtok()
的标记化循环。然后处理 first-character 大写,然后将输出限制为 20 个字符 per-line.
您可以执行以下操作:
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#define CPL 20 /* chars per-line, if you need a constant, #define one (or more) */
#define MAXC 1024
#define DELIM " \t\r\n"
void putcharCPL (int c, int *n)
{
if (*n == CPL) { /* if n == limit */
putchar ('\n'); /* output '\n' */
*n = 0; /* reset value at mem address 0 */
}
putchar (c); /* output character */
(*n)++; /* increment value at mem address */
}
int main (int argc, char **argv) {
char line[MAXC]; /* buffer to hold each line */
int n = 0; /* no. of chars ouput in line */
/* use filename provided as 1st argument (stdin by default) */
FILE *fp = argc > 1 ? fopen (argv[1], "r") : stdin;
if (!fp) { /* validate file open for reading */
perror ("file open failed");
return 1;
}
while (fgets (line, MAXC, fp)) /* read each line and tokenize line */
for (char *tok = strtok (line, DELIM); tok; tok = strtok (NULL, DELIM)) {
putcharCPL (toupper(*tok), &n); /* convert 1st char to upper */
for (int i = 1; tok[i]; i++) /* output rest unchanged */
putcharCPL (tok[i], &n);
}
putchar ('\n'); /* tidy up with newline */
if (fp != stdin) /* close file if not stdin */
fclose (fp);
}
(相同的输出)
putcharCPL()
函数只是一个帮助程序,它检查是否已输出 20
个字符,如果是,则输出 '\n'
并重置计数器。然后它输出当前字符并将计数器加一。传递一个指向计数器的指针,以便它可以在函数内更新,使更新后的值在 main()
.
中可用
检查一下,如果您还有其他问题,请告诉我。
脚注:
1. 根据您的 gcc 版本,源代码中设置 read-buffer 大小的常量可能是 _IO_BUFSIZ
。 _IO_BUFSIZ
在此处更改为 BUFSIZ
:glibc commit 9964a14579e5eef9 对于 Linux BUFSIZE
定义为 8192
(512
在 Windows).
我在使用 strtok 函数时遇到了一些问题。 作为练习,我必须通过排除空格、将首字母转换为大写字母并在一行中打印不超过 20 个字符来处理文本文件。
这是我的代码片段:
fgets(sentence, SIZE, f1_ptr);
char *tok_ptr = strtok(sentence, " \n"); //tokenazing each line read
tok_ptr[0] = toupper(tok_ptr[0]); //initials to capital letters
int num = 0, i;
while (!feof(f1_ptr)) {
while (tok_ptr != NULL) {
for (i = num; i < strlen(tok_ptr) + num; i++) {
if (i % 20 == 0 && i != 0) //maximum of 20 char per line
fputc('\n', stdout);
fputc(tok_ptr[i - num], stdout);
}
num = i;
tok_ptr = strtok(NULL, " \n");
if (tok_ptr != NULL)
tok_ptr[0] = toupper(tok_ptr[0]);
}
fgets(sentence, SIZE + 1, f1_ptr);
tok_ptr = strtok(sentence, " \n");
if (tok_ptr != NULL)
tok_ptr[0] = toupper(tok_ptr[0]);
}
文本只是一堆行,我只是作为参考显示:
Watch your thoughts ; they become words .
Watch your words ; they become actions .
Watch your actions ; they become habits .
Watch your habits ; they become character .
Watch your character ; it becomes your destiny .
这是我最后得到的:
WatchYourThoughts;Th
eyBecomeWords.WatchY
ourWords;THeyBecomeA
ctions.WatchYourActi
ons;TheyBecomeHabits
.WatchYourHabits;The
yBecomeCharacteR.Wat
chYourCharacter;ItBe
comesYourDEstiny.Lao
-Tze
最终结果大部分是正确的,但有时(例如 中的“他们”变成了(并且仅在这种情况下)或“命运”)单词未正确标记.因此,例如,在我进行的操作之后,“他们”被分成“t”和“嘿”,导致 THey(另一个实例中的 DEstiny)。 是一些错误还是我遗漏了什么?可能我的代码效率不高,某些条件可能最终变得很关键...
谢谢你的帮助,没什么大不了的,我只是不明白为什么会发生这种行为。
从专业的角度来看,这实际上是一个比某些评论可能暗示的更有趣的 OP,尽管问题的 'newcomer' 方面有时可能会引发相当深刻的、被低估的问题。
有趣的是,在我的平台(W10、MSYS2、gcc v.10.2)上,您的代码运行良好,结果正确:
WatchYourThoughts;Th
eyBecomeWords.WatchY
ourWords;TheyBecomeA
ctions.WatchYourActi
ons;TheyBecomeHabits
.WatchYourHabits;The
yBecomeCharacter.Wat
chYourCharacter;ItBe
comesYourDestiny.
那么首先,恭喜新人:你的编码还不错。
这表明不同的编译器可能会或可能不会防止有限的不当编码或规范滥用,可能会或可能不会保护堆栈或堆。
这就是说,@Andrew Henle 的评论指向一个关于 feof
的启发性答案是非常相关的。
如果您遵循它并检索您的 feof
测试,只需将其向下移动 after 读取检查,not before(如下所示)。您的代码应该会产生更好的结果(注意:我只会对您的代码进行最少的改动,故意忽略较小的问题):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <ctype.h>
#define SIZE 100 // add some leeway to avoid off-by-one issues
int main()
{
FILE* f1_ptr = fopen("C:\Users\Public\Dev\test_strtok", "r");
if (! f1_ptr)
{
perror("Open issue");
exit(EXIT_FAILURE);
}
char sentence[SIZE] = {0};
if (NULL == fgets(sentence, SIZE, f1_ptr))
{
perror("fgets issue"); // implementation-dependent
exit(EXIT_FAILURE);
}
errno = 0;
char *tok_ptr = strtok(sentence, " \n"); //tokenizing each line read
if (tok_ptr == NULL || errno)
{
perror("first strtok parse issue");
exit(EXIT_FAILURE);
}
tok_ptr[0] = toupper(tok_ptr[0]); //initials to capital letters
int num = 0;
size_t i = 0;
while (1) {
while (1) {
for (i = num; i < strlen(tok_ptr) + num; i++) {
if (i % 20 == 0 && i != 0) //maximum of 20 char per line
fputc('\n', stdout);
fputc(tok_ptr[i - num], stdout);
}
num = i;
tok_ptr = strtok(NULL, " \n");
if (tok_ptr == NULL) break;
tok_ptr[0] = toupper(tok_ptr[0]);
}
if (NULL == fgets(sentence, SIZE, f1_ptr)) // let's get away whith annoying +1,
// we have enough headroom
{
if (feof(f1_ptr))
{
fprintf(stderr, "\n%s\n", "Found EOF");
break;
}
else
{
perror("Unexpected fgets issue in loop"); // implementation-dependent
exit(EXIT_FAILURE);
}
}
errno = 0;
tok_ptr = strtok(sentence, " \n");
if (tok_ptr == NULL)
{
if (errno)
{
perror("strtok issue in loop");
exit(EXIT_FAILURE);
}
break;
}
tok_ptr[0] = toupper(tok_ptr[0]);
}
return 0;
}
$ ./test
WatchYourThoughts;Th
eyBecomeWords.WatchY
ourWords;TheyBecomeA
ctions.WatchYourActi
ons;TheyBecomeHabits
.WatchYourHabits;The
yBecomeCharacter.Wat
chYourCharacter;ItBe
comesYourDestiny.
Found EOF
您的代码中有大量错误,over-complicating 问题出在您身上。最紧迫的错误是Why is while ( !feof (file) ) always wrong? 为什么?在循环中跟踪 execution-path。您尝试使用 fgets()
进行阅读,然后您使用 sentence
而不知道是否已到达 EOF
调用 tok_ptr = strtok(sentence, " \n");
,然后再开始检查 feof(f1_ptr)
当你真正达到 EOF
时会发生什么?那是 “为什么 while ( !feof (file) ) 总是错误的?” 相反,你总是想用读取的 return 来控制你的 read-loop您正在使用的功能,例如while (fgets(sentence, SIZE, f1_ptr) != NULL)
您实际需要代码做什么?
更大的问题是,为什么您 over-complicating 是 strtok
和数组(以及 fgets()
的问题)?想想你需要做什么:
- 读取文件中的每个字符,
- 如果是空格,忽略它,设置in-word标志
false
, - if a non-whitespace,if word中的第一个字符,将其大写,输出该字符,设置in-word标志
true
并增加输出到当前行的字符数, 最后 - 如果是第20个字符输出,则输出一个换行符并将计数器清零。
您的 C-toolbox 需要的 bare-minimum 工具是 fgetc()
、isspace()
和 toupper()
来自 ctype.h
,一个计数器输出的字符数,以及一个标志,用于判断该字符是否是空格后的第一个 non-whitespace 字符。
实现逻辑
这样问题就很简单了。读取一个字符,是空格吗,设置你的in-word标志false
,否则如果你的in-word标志是false
,将它大写,输出字符,设置你的in-word flag true
,增加你的字数。您需要做的最后一件事是检查您的 character-count 是否已达到限制,如果是,则输出 '\n'
并将您的 character-count 重置为零。重复,直到 运行 个字符不足。
您可以将其转换为类似于以下内容的代码:
#include <stdio.h>
#include <ctype.h>
#define CPL 20 /* chars per-line, if you need a constant, #define one (or more) */
int main (int argc, char **argv) {
int c, in = 0, n = 0; /* char, in-word flag, no. of chars output in line */
/* use filename provided as 1st argument (stdin by default) */
FILE *fp = argc > 1 ? fopen (argv[1], "r") : stdin;
if (!fp) { /* validate file open for reading */
perror ("file open failed");
return 1;
}
while ((c = fgetc(fp)) != EOF) { /* read / validate each char in file */
if (isspace(c)) /* char is whitespace? */
in = 0; /* set in-word flag false */
else { /* otherwise, not whitespace */
putchar (in ? c : toupper(c)); /* output char, capitalize 1st in word */
in = 1; /* set in-word flag true */
n++; /* increment character count */
}
if (n == CPL) { /* CPL limit reached? */
putchar ('\n'); /* output newline */
n = 0; /* reset cpl counter */
}
}
putchar ('\n'); /* tidy up with newline */
if (fp != stdin) /* close file if not stdin */
fclose (fp);
}
例子Use/Output
鉴于你的输入文件存储在我的计算机上 dat/text220.txt
,你可以生成你正在寻找的输出:
$ ./bin/text220 dat/text220.txt
WatchYourThoughts;Th
eyBecomeWords.WatchY
ourWords;TheyBecomeA
ctions.WatchYourActi
ons;TheyBecomeHabits
.WatchYourHabits;The
yBecomeCharacter.Wat
chYourCharacter;ItBe
comesYourDestiny.
(代码的可执行文件被编译为 bin/text220
,我通常为数据、目标文件和可执行文件保留单独的 dat
、obj
和 bin
目录保持源代码目录干净)
注意: 默认从 stdin
读取 如果没有文件名作为程序的第一个参数提供,您可以使用您的程序直接读取输入,例如
$ echo "my dog has fleas - bummer!" | ./bin/text220
MyDogHasFleas-Bummer
!
不需要花哨的字符串函数,只需一个循环、一个字符、一个标志和一个计数器 -- 其余的只是算术运算。总是值得尝试将您的编程问题归结为基本步骤,然后环顾您的 C-toolbox 并为每个基本步骤找到合适的工具。
使用strtok
不要误会我的意思,使用 strtok
没有任何问题,在这种情况下它是一个相当简单的解决方案——我的意思是对于简单的 character-oriented string-processing,它通常只是简单地循环遍历行中的字符。使用数组 fgets()
和 strtok()
不会获得任何效率,从文件中读取的内容已经放入 BUFSIZ
1[=129= 的缓冲区中].
如果你确实想使用 strtok()
,你应该用 fgets()
中的 return 控制你 read-loop 然后你可以用 [=41= 标记化] 还在每个点检查其 return。带有 fgets()
的 read-loop 和带有 strtok()
的标记化循环。然后处理 first-character 大写,然后将输出限制为 20 个字符 per-line.
您可以执行以下操作:
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#define CPL 20 /* chars per-line, if you need a constant, #define one (or more) */
#define MAXC 1024
#define DELIM " \t\r\n"
void putcharCPL (int c, int *n)
{
if (*n == CPL) { /* if n == limit */
putchar ('\n'); /* output '\n' */
*n = 0; /* reset value at mem address 0 */
}
putchar (c); /* output character */
(*n)++; /* increment value at mem address */
}
int main (int argc, char **argv) {
char line[MAXC]; /* buffer to hold each line */
int n = 0; /* no. of chars ouput in line */
/* use filename provided as 1st argument (stdin by default) */
FILE *fp = argc > 1 ? fopen (argv[1], "r") : stdin;
if (!fp) { /* validate file open for reading */
perror ("file open failed");
return 1;
}
while (fgets (line, MAXC, fp)) /* read each line and tokenize line */
for (char *tok = strtok (line, DELIM); tok; tok = strtok (NULL, DELIM)) {
putcharCPL (toupper(*tok), &n); /* convert 1st char to upper */
for (int i = 1; tok[i]; i++) /* output rest unchanged */
putcharCPL (tok[i], &n);
}
putchar ('\n'); /* tidy up with newline */
if (fp != stdin) /* close file if not stdin */
fclose (fp);
}
(相同的输出)
putcharCPL()
函数只是一个帮助程序,它检查是否已输出 20
个字符,如果是,则输出 '\n'
并重置计数器。然后它输出当前字符并将计数器加一。传递一个指向计数器的指针,以便它可以在函数内更新,使更新后的值在 main()
.
检查一下,如果您还有其他问题,请告诉我。
脚注:
1. 根据您的 gcc 版本,源代码中设置 read-buffer 大小的常量可能是 _IO_BUFSIZ
。 _IO_BUFSIZ
在此处更改为 BUFSIZ
:glibc commit 9964a14579e5eef9 对于 Linux BUFSIZE
定义为 8192
(512
在 Windows).