Различные типы ввода для fscanf
Мое понимание fscanf:
захватывает строку из файла и, основываясь на формате, сохраняет ее в строку.
Тем не менее, есть три (казалось бы, разных) способа передать "строки" (массив символов).
Некоторые предположения:
1. fp является действительным указателем FILE.
2. Файл содержит 1 строку с надписью "Нечто"
Указатель с выделенной памятью
char* temp = malloc(sizeof(char) * 1); // points to some small part in mem.
int resp = fscanf(fp,"%s", temp);
printf("Trying to print: %s\n",temp); // prints "Something" (that's what's in the file)
Массив с заданной длиной (отличается от указателя!)
char temp[100]; // this buffer MUST be big enough, or we get segmentation fault
int resp = fscanf(fp,"%s", temp);
printf("Trying to print: %s\n",temp); // prints "Something" (that's what's in the file)
Нулевой указатель
char* temp; // null pointer
int resp = fscanf(fp,"%s", temp);
printf("Trying to print: %s\n",temp); // Crashes, segmentation fault
Итак, возникло несколько вопросов!
- Как указатель с malloc 1 может содержать более длинные тексты?
- Поскольку содержимое указателя не имеет значения, почему происходит сбой нулевого указателя? Я ожидаю, что выделенный указатель также потерпит крах, поскольку он указывает на небольшой фрагмент памяти.
- Почему работает указатель, но массив (
char temp[1];
) вылетает?
Редактировать:
Я хорошо знаю, что вам нужно передать достаточно большой буфер для хранения данных из строки, мне было интересно, почему он все еще работает и не дает сбоя в других ситуациях.
4 ответа
Почему указатель с malloc 1 может содержать более длинные тексты?
В теории это не может быть, не вызывая неопределенного поведения. На практике, однако, когда вы выделяете один байт, распределитель предоставляет вам небольшой кусок памяти наименьшего размера, который он поддерживает, что обычно достаточно для 8.10 символов без сбоя. Дополнительная память служит "заполнением", которое предотвращает сбой (но это все еще неопределенное поведение).
Поскольку содержимое указателя, кажется, не имеет значения, почему происходит сбой нулевого указателя, я ожидал бы, что выделенный указатель также потерпит крах, поскольку он указывает на небольшой фрагмент памяти.
Нулевого указателя, с другой стороны, недостаточно даже для пустой строки, потому что вам нужно место для нулевого терминатора. Следовательно, это гарантированный UB, который проявляется как сбой на большинстве платформ.
Почему работает указатель, но массив (
char temp[1]
) вылетает?
Потому что массивы распределяются без дополнительной памяти. Обратите внимание, что сбой не гарантируется, поскольку за массивом могут следовать неиспользуемые байты памяти, которые ваша строка может повредить без каких-либо последствий.
Мое понимание fscanf:
захватывает строку из файла и, основываясь на формате, сохраняет ее в строку.
Нет, это содержит некоторые серьезные и важные заблуждения. fscanf()
читает из файла в соответствии с указанным форматом, чтобы присвоить значения некоторым или всем объектам, на которые указывают его третий и последующие аргументы. Он не обязательно читает целую строку, но, с другой стороны, может читать больше одной.
В вашем конкретном использовании,
int resp = fscanf(fp,"%s", temp);
, он пытается пропустить любой начальный пробел, включая, но не ограничиваясь, пустые и пустые строки, а затем считывает символы в массив указательных символов, вплоть до первого символа пробела или конца файла. Ни при каких обстоятельствах он не будет использовать терминатор строки, из которой он заполняет содержимое массива, но он даже не доберется так далеко, если в строке есть другие пробелы после хотя бы одного непробельного символа (хотя это не случай в конкретном примере ввода вы описываете).
Тем не менее, есть три (казалось бы, разных) способа передать "строки" (массив символов).
Строки не являются фактическим типом данных в C. Массивы символов таковы, но такие массивы не являются "строками" в смысле C, если они не содержат хотя бы один нулевой символ. Кроме того, в этом случае строковые функции C по большей части работают только с частями таких массивов вплоть до первого нуля включительно, поэтому именно эти части лучше всего охарактеризовать как "строки".
Существует несколько способов получить хранилище для последовательностей символов, которые можно считать строками, но есть только один способ передать их: с помощью указателя на их первый символ. Получаете ли вы хранение, объявляя массив символов, строковым литералом или выделяя для него память, доступ к содержимому осуществляется только через указатели. Даже когда вы объявляете массив символов и получаете доступ к элементам, применяя оператор индекса, []
на имя переменной массива, вы все еще используете указатель для доступа к содержимому.
- Почему указатель с malloc 1 может содержать более длинные тексты?
Указатель не содержит ничего, кроме самого себя. Это пространство, на которое он указывает, содержит что-то еще, например, текст. Если вы выделяете только один байт, выделенное пространство может содержать только один байт. Если вы переполните этот один байт, пытаясь написать более длинную последовательность символов, на которую указывает указатель, то вы вызываете неопределенное поведение. В частности, C не гарантирует, что будет сгенерирована ошибка, или что программа не будет вести себя так, как вы ожидаете, но возможны любые разрушения без ограничений.
- Поскольку содержимое указателя, кажется, не имеет значения, почему происходит сбой нулевого указателя, я ожидал бы, что выделенный указатель также потерпит крах, поскольку он указывает на небольшой фрагмент памяти.
Попытка разыменования недопустимого указателя, включая, но не ограничиваясь этим, нулевой указатель, также приводит к неопределенному поведению. Авария вполне в пределах возможного поведения. C не гарантирует сбой в этом случае, но это надежно обеспечивается некоторыми реализациями.
- Почему работает указатель, но массив (char temp[1];) падает?
Вы не демонстрируете альтернативу массиву из 1 символа, но опять же, превышение границ объекта - в данном случае массива - приводит к неопределенному поведению. Он не определен, поэтому неоправданно полагать, что поведение будет таким же, как и при превышении границ выделенного объекта, или даже то, что любое из этих поведений будет согласованным.
Тем не менее, есть три (казалось бы, разных) способа передать "строки" (массив символов).
Для передачи C-"строки" в scanf()
У друзей есть только один способ: передать ему адрес достаточной действительной памяти.
Если вы этого не сделаете, код вызовет бесславное неопределенное поведение, что означает, что может произойти что угодно, от сбоя до, казалось бы, работы нормально.
Потому что нулевые указатели не выделяются памятью.
Когда вы запрашиваете небольшой кусок памяти, он выделяется из блока памяти, который называется "куча". Куча всегда выделяется и освобождается в единицах блоков или страниц, которые всегда будут немного больше, чем несколько байтов, обычно несколько килобайт.
Поэтому, когда вы выделяете память с new
или определив массив (маленький), вы получите часть памяти в куче. Фактически доступное пространство больше и может (часто) превышать запрошенную вами сумму, поэтому практически безопасно написать (и прочитать) больше, чем запрошено. Но теоретически, это UB и должно вызвать сбой программы.
Когда вы создаете нулевой указатель, он указывает на 0, недопустимый адрес, с которого невозможно прочитать или записать. Таким образом, гарантируется, что программа потерпит крах, часто из-за ошибки сегментации.
Маленькие массивы могут зависать чаще, чем new
а также malloc
потому что они не всегда выделяются из кучи, и могут идти без лишнего пробела после них, поэтому более опасно писать за предел. Однако они часто предшествуют неиспользуемым (нераспределенным) областям памяти, поэтому иногда ваша программа может не аварийно завершить работу, а вместо этого получить поврежденные данные.