Используя Kubuntu 22.04 LTS, Kate v22.04.3 и gcc v11.3.0, я разработал небольшую программу для исследования использования strtok() для токенизации строк, которая показана ниже.
#include <stdio.h>
#include <string.h>
int main(void)
{
char inString[] = ""; // string read in from keyboard.
char * token = ""; // A word (token) from the input string.
char delimiters[] = " ,"; // Items that separate words (tokens).
// explain nature of program.
printf("This program reads in a string from the keyboard"
"\nand breaks it into separate words (tokens) which"
"\nare then output one token per line.\n");
printf("\nEnter a string: ");
scanf("%s", inString);
/* get the first token */
token = strtok(inString, delimiters);
/* Walk through other tokens. */
while (token != NULL)
{
printf("%s", token);
printf("\n");
// Get next token.
token = strtok(NULL, delimiters);
}
return 0;
}
Из различных веб-страниц, которые я просматривал, кажется, что я правильно отформатировал вызов функции strtok(). При первом запуске программа выдает следующий результат.
$ ./ex6_2
This program reads in a string from the keyboard
and breaks it into separate words (tokens) which
are then output one token per line.
Enter a string: fred , steve , nick
f
ed
При втором запуске он выдал следующий результат.
$ ./ex6_2
This program reads in a string from the keyboard
and brakes it into separate words (tokens) which
are then output one token per line.
Enter a string: steve , barney , nick
s
eve
*** stack smashing detected ***: terminated
Aborted (core dumped)
Последующие запуски показали, что программа как бы запускается, как и в первом случае выше, если первое слово/токен содержит только четыре символа. Однако, если первое слово/токен содержало пять или более символов, происходило разрушение стека.
Учитывая, что для доступа к токенам используется «char *», почему:
а) разделяется ли первый токен (в каждом случае) на втором символе?
б) последующие токены (в каждом случае) не выводятся?
c) приводит ли первое слово/токен из более чем четырех символов к разрушению стека?
Стюарт
scanf("%s", inString);
не будет вводить строки с пробелами.
OT: char * token = "";
странно, скорее должно быть char * token = NULL;
, а тут вообще не надо его инициализировать. Самым идиоматичным способом было бы удалить char * token = "";
все вместе и оставить только char *token = strtok(inString, delimiters);
@Marco: переполнение стека сильно отличается от переполнения буфера в стеке. Вы неправильно используете термин «переполнение стека» для последнего.
@Stuart Когда вы написали char inString[]
, вы думали, что это означало «выделить inString
как строку, которая настолько велика, насколько это необходимо для всего, что в нее читается?» C не имеет строк, которые работают таким образом.
@AndreasWenzel Я не такой. ОП сказал, что получил следующее сообщение: *** stack smashing detected ***:
что означает, что стек поврежден.
@AndreasWenzel Если вы погуглите stack smashing
, первый результат будет A cyberattack that causes a stack buffer overflow.
.
@Marco: В своем первом комментарии вы использовали термин «переполнение стека», а не «переполнение буфера стека». переполнение стека сильно отличается от переполнения буфера стека. Вы путаете эти два термина.
@AndreasWenzel Вы не правы. См. Переполнение стека (значения): Переполнение стека может также означать: переполнение буфера стека, когда программа записывает в адрес памяти в стеке вызовов программы за пределами предполагаемой структуры данных; обычно буфер фиксированной длины.
@Marco: Цель страниц значений неоднозначности в Википедии — помочь вам найти статью, которую вы ищете. Поэтому, когда, например, на странице утверждается, что термин А «может относиться» к термину Б, не обязательно правильно делать вывод, что термин А является подходящим способом ссылки на термин Б. Например, термин А также может быть распространенным неверным способом. ссылки на термин B. Я предлагаю вам прочитать сами статьи, чтобы определить, какие термины подходят.
@AndreasWenzel Я не согласен. Устранение неоднозначности - это процесс определения того, какое значение слова используется в контексте. В этом контексте должно быть совершенно ясно, что имелось в виду «переполнение буфера стека». Если достаточное количество людей называют переполнение буфера стека «переполнением стека» [в контексте], вам придется принять это. Вы можете быть технически правы, но опять же, это не имеет значения. Единственное, что вы могли бы отметить, это то, что мое заявление было двусмысленным, но не ошибочным. Это не «неправильно» ссылаться на переполнение буфера стека переполнением стека, иначе не существовало бы неоднозначности в Википедии.
@AndreasWenzel Однако я благодарен, что вы указали на разницу. Я знаю, что переполнение стека вызовов отличается от переполнения буфера в стеке. Я не знал, что людей так волнует разница.
Единственное, что имеет значение, это то, что я правильно назвал переполнение буфера стека «переполнением стека».
@Марко: Ты прав. Я знаю, что допустимо не устанавливать размер массива, если для инициализации массива предоставляется фактическая строка. Я предполагаю, что мои мысли были неясными.
@n.m.: Спасибо за это. Я не знал об этом.
@Jabberwocky: Вы правы в том, что указатель должен был быть инициализирован с помощью NULL. С каких это пор указатель(!) инициализируется строкой?!! Шиш. Что касается вашего предложения инициализировать указатель вызовом strtok(), это кажется лучшей идеей. С вашего позволения, я воспользуюсь этим.
@Andreas Wenzel: Если вы перечитаете мою исходную публикацию, вы увидите, что я никогда не упоминал ни о переполнении стека, ни о переполнении буфера. Это была система, которая сообщила «*** обнаружен сбой стека ***», о чем я затем упомянул в следующем абзаце.
@Steve Summit: Не совсем так. Я думал, что выделяю место для строки нулевой длины, которая затем будет расширена в соответствии с тем, что было захвачено функцией scanf(). Да, я узнаю, что струны C определенно не действуют таким образом. Хо хм. Я думаю, что я немного устал/не ясно мыслил, когда кодировал этот бит.
@Stuart: Да, вы правы, что никогда не использовали термин «переполнение стека». Если вы перечитаете мои комментарии, вы увидите, что они были адресованы пользователю по имени «Марко», который использовал (неверный) термин «переполнение стека» в своем первом комментарии к вашему вопросу.
Пожалуйста, примите мои извинения за то, что я забыл, кому был адресован ваш комментарий. Прочитав более дюжины ответов, я думаю, что немного запутался.
Это объявление массива символов
char inString[] = "";
эквивалентно
char inString[1] = { '\0' };;
То есть он объявляет массив только с одним элементом, который может хранить только пустую строку. Таким образом, любая попытка прочитать строку в этом массиве символов с помощью этого вызова scanf
scanf("%s", inString);
вызывает неопределенное поведение.
Вам нужно указать количество элементов намного больше. Например
enum { N = 100 };
char inString[N] = "";
Эта инициализация указателя
char * token = "";
не имеет большого смысла. Лучше написать например
char * token = NULL
;
Этот призыв scanf
scanf("%s", inString);
может прочитать только одно слово, представляющее собой последовательность символов, разделенных пробелами.
Вместо этого напишите, например
scanf( " %99[^\n]", inString);
Имеет смысл включить символ табуляции '\t' в список разделителей
const char *delimiters = " \t,";
Вместо этих звонков printf
printf("%s", token);
printf("\n");
будет проще написать
puts( token );
Большое спасибо за разъяснения и ваши предложения. Будет ли scanf( "%99[^\n]", inString)
захватывать строку, содержащую пробелы? Я только что узнал, что scan() захватывает символы до первого пробела, но не включает его.
@Stuart Этот вызов scanf считывает все символы, включая пробелы, до тех пор, пока не встретится символ новой строки. . Также безопаснее будет написать scanf( " %99[^\n]", inString); Обратите внимание на начальный пробел в строке формата. Это позволяет пропускать символы пробела, оставшиеся в буфере после ввода других данных, например, если перед этим вызовом есть вызов типа scanf( "%d", &number ); читать целое число.
@Stuart Потому что после этого вызова символ новой строки '\n' будет оставлен в буфере, а вызов scanf, который читает строку, будет считывать пустую строку, потому что он сразу встречает символ новой строки.
Спасибо за ваши дальнейшие разъяснения. Они очень полезны.
Декларация
char inString[] = "";
эквивалентно:
char inString[1] = "";
Это означает, что вы выделяете массив только из одного элемента, поэтому в нем есть место только для хранения одного символа.
Вызов функции
scanf("%s", inString);
требует, чтобы аргумент функции inString
указывал на буфер памяти, достаточно большой для хранения согласованных входных данных. Ваша программа нарушает это требование, поскольку в буфере памяти есть место только для одного символа (конечный нулевой символ). Поэтому он может хранить только строки нулевой длины.
Нарушая требование, ваша программа вызывает неопределенное поведение , а это значит, что может произойти что угодно, включая наблюдаемое вами странное поведение. Функция scanf
, вероятно, переполняет буфер inString
, перезаписывая другие важные данные в стеке вашей программы, что приводит к неправильному поведению. Это называется "разгром стека".
Чтобы это исправить, вы должны дать массиву inString
больше места, например, изменив строку
char inString[] = "";
к:
char inString[200] = "";
Однако в этом случае, если пользователь введет более 200 символов в качестве одного слова, у вас снова возникнет та же проблема, и ваша программа может рухнуть. Поэтому вы можете дополнительно ограничить количество символов, соответствующих scanf
, до 199
символов (200
, включая завершающий нулевой символ). Таким образом, вы можете гарантировать, что пользователь не сможет сломать вашу программу.
Вы можете добавить такой лимит следующим образом:
scanf("%199s", inString);
Обратите внимание, однако, что спецификатор %s
будет соответствовать только одному слову. Если вы хотите прочитать всю строку ввода, вы можете использовать функцию fgets вместо scanf
.
Большое спасибо за очевидно столь необходимое разъяснение. Я думаю, что буду(!) использовать fgets() вместо scanf(). Вы говорите, что «... спецификатор %s будет соответствовать только одному слову». Если это правда, какой смысл использовать «большое число» в «%s», если слова после первого пробела игнорируются функцией scanf()?
@Stuart: Вы правы в том, что в большинстве случаев пользователь, вероятно, никогда не введет слово длиннее 200 символов, поэтому использования %s
вместо %199s
, вероятно, будет достаточно. Однако если вы программируете, например, веб-сервер, то злоумышленник сможет вывести из строя вашу серверную программу, введя слово длиннее 200 символов. Вы можете запретить пользователю делать это, используя %199s
вместо %s
. Следовательно, вам решать, хотите ли вы, чтобы пользователь не мог сломать вашу программу, или это не имеет значения.
@Stuart: Если один из ответов решил вашу проблему, вы можете рассмотреть возможность принятия одного из ответов. См. эту официальную справочную страницу для получения дополнительной информации: Что мне делать, когда кто-то отвечает на мой вопрос?
@Stuart: Если вы решите использовать fgets
, вы, вероятно, столкнетесь с проблемой fgets
хранения символа \n
в конце строки в строке. Поэтому вы можете прочитать это о том, как его удалить: Удаление завершающего символа новой строки из ввода fgets()
char inString[] = "";
вызывает переполнение стека. Вы не указали длину строки, поэтому она не может содержать строку длиннее 0.