Динамически выделять введенную пользователем строку

Я пытаюсь написать функцию, которая выполняет следующие действия:

  • Запустите цикл ввода, выводя '> ' на каждой итерации.
  • Возьмите все, что вводит пользователь (неизвестная длина), и прочитайте его в массив символов, динамически выделяя размер массива, если это необходимо. Введенная пользователем строка будет заканчиваться символом новой строки.
  • Добавьте нулевой байт '\0' в конец массива символов.
  • Цикл завершается, когда пользователь вводит пустую строку: '\n'

Вот что я сейчас написал:

void input_loop(){
    char *str = NULL;

    printf("> ");

    while(printf("> ") && scanf("%a[^\n]%*c",&input) == 1){

        /*Add null byte to the end of str*/

        /*Do stuff to input, including traversing until the null byte is reached*/

        free(str);
        str = NULL;
    }
    free(str);
    str = NULL;
}

Теперь я не слишком уверен, как добавить нулевой байт в конец строки. Я думал примерно так:

last_index = strlen(str);
str[last_index] = '\0';

Но я не слишком уверен, что это сработает. Я не могу проверить, будет ли это работать, потому что я сталкиваюсь с этой ошибкой, когда пытаюсь скомпилировать свой код:

warning: ISO C does not support the 'a' scanf flag [-Wformat=]

Итак, что я могу сделать, чтобы мой код работал?

РЕДАКТИРОВАТЬ: изменение scanf("%a[^\n]%*c",&input) == 1 на scanf("%as[^\n]%*c",&input) == 1 дает мне ту же ошибку.


person JavascriptLoser    schedule 16.05.2015    source источник
comment
Вы не можете использовать strlen для получения последнего индекса, предполагая, что ваша строка не имеет нуля в конце. Если в конце есть ноль, то зачем вы его повторно добавляете?   -  person ozdrgnaDiies    schedule 16.05.2015
comment
input опечатка как str.   -  person BLUEPIXY    schedule 04.06.2017


Ответы (4)


Прежде всего, строки формата scanf не используют регулярные выражения, поэтому я не думаю, что что-то близкое к тому, что вы хотите, сработает. Что касается ошибки, которую вы получаете, согласно моему надежному руководству, %a флаг преобразования предназначен для чисел с плавающей запятой, но он работает только на C99 (и ваш компилятор, вероятно, настроен для C90)

Но тогда у вас есть большая проблема. scanf ожидает, что вы передадите ему ранее выделенный пустой буфер для его заполнения прочитанным вводом. Он не выделяет для вас scstring, поэтому ваши попытки инициализировать str значением NULL и соответствующими освобождениями не будут работать со scanf.

Самое простое, что вы можете сделать, это отказаться от n строк произвольной длины. Создайте большой буфер и запретите входы, которые длиннее этого.

Затем вы можете использовать функцию fgets для заполнения буфера. Чтобы проверить, удалось ли прочитать всю строку, проверьте, заканчивается ли ваша строка на "\n".

char str[256+1];
while(true){
    printf("> ");
    if(!fgets(str, sizeof str, stdin)){
        //error or end of file
        break;
    }

    size_t len = strlen(str);
    if(len + 1 == sizeof str){
        //user typed something too long
        exit(1);
    }

    printf("user typed %s", str);
}

Другой альтернативой является использование нестандартной библиотечной функции. Например, в Linux есть функция getline, которая считывает полную строку ввода с помощью malloc. за кулисами.

person hugomg    schedule 16.05.2015
comment
Я не совсем уверен, как использовать fgets. Это кажется королем путаницы для меня. Можете ли вы объяснить это мне, как будто мне пять? РЕДАКТИРОВАТЬ: Кроме того, как я могу включить его в свой цикл ввода? - person JavascriptLoser; 16.05.2015
comment
Вы просто заранее выделяете строку с достаточным пространством в соответствии с вашими потребностями. Для ввода выберите достаточно большое число, например 100, 256, 512 и т. д., в зависимости от того, что вы хотите. fgets принимает 3 параметра: место для размещения строки, максимальная длина строки + нуль, который она добавляет автоматически, и место для чтения ввода. Для ввода вы можете указать stdin для чтения из консоли или дескриптора файла, если он у вас есть. Так, например, если бы у вас был char str[512];, вы бы назвали fgets(str, 512 - 1, stdin);. -1 в размере соответствует нулю. fgets возвращает null в случае ошибки. - person ozdrgnaDiies; 16.05.2015
comment
@ozdrgnaDiies Добавляет ли fgets нулевой байт в конец строки? - person JavascriptLoser; 16.05.2015
comment
@PythonNewb Да, это так. Подробнее об этом можно прочитать здесь: cplusplus.com/reference/cstdio/fgets - person ozdrgnaDiies; 16.05.2015
comment
@ozdrgnaDiies: Незначительный момент: вам не нужно освобождать место для нулевого байта; fgets позаботится об этом за вас. fgets(str, sizeof(str), stdin); должно быть хорошо в вашем примере. - person M Oehm; 16.05.2015
comment
добавил несколько примеров. пожалуйста, проверьте, не испортил ли я условие ошибки с ошибкой off-by-one. - person hugomg; 16.05.2015
comment
@MOehm Да, вы правы, я не знаю, откуда я это взял. - person ozdrgnaDiies; 16.05.2015
comment
@ozdrgnaDiies: Вероятно, из-за непоследовательной обработки строк в библиотеке C. В scanf вы должны явно добавить -1, что очень раздражает, - person M Oehm; 16.05.2015

Нет проверки ошибок, не забудьте освободить указатель, когда закончите с ним. Если вы используете этот код для чтения огромных строк, вы заслуживаете всей боли, которую он вам принесет.

#include <stdio.h>
#include <stdlib.h>

char *readInfiniteString() {
    int l = 256;
    char *buf = malloc(l);
    int p = 0;
    char ch;

    ch = getchar();
    while(ch != '\n') {
        buf[p++] = ch;
        if (p == l) {
            l += 256;
            buf = realloc(buf, l);
        }
        ch = getchar();
    }
    buf[p] = '\0';

    return buf;
}

int main(int argc, char *argv[]) {
    printf("> ");
    char *buf = readInfiniteString();
    printf("%s\n", buf);
    free(buf);
}
person Will Hartung    schedule 16.05.2015
comment
Людям, которые хотят использовать это, следует отметить, что если realloc выйдет из строя, buf будет потерян, и произойдет утечка памяти. Вместо этого вы должны назначить результат realloc временному указателю для проверки ошибок и переназначить его после этого. - person ozdrgnaDiies; 16.05.2015
comment
@ozdrgnaDiies: Это зависит от того, что вы хотите делать в случае неудачи. Вы возвращаете усеченную строку? Если да, то как об этом узнает вызывающий код? Часто люди просто выручают и выходят из программы. Никакой дополнительной работы здесь не требуется. - person M Oehm; 16.05.2015
comment
возможная настройка - сделать l *= SOME_CONSTANT_FACTOR вместо l += 256. Таким образом, вы избежите квадратичного времени выполнения, если есть очень длинная строка ввода. - person hugomg; 16.05.2015
comment
getchar будет возвращать значение от 0 до UCHAR_MAX (включительно) в случае успеха (обычно одно из 256 значений) или EOF (что обычно приводит к одному из 257 значений), когда это указывает на ошибку. Если ch обычно не может хранить одно из 257 различных значений, то вы рискуете не распознать, когда EOF или ошибка была отмечена... не то, чтобы это имело значение, поскольку ваш цикл все равно не пытается это проверить. Я настоятельно рекомендую изменить ch на int (как также предлагает руководство), для начала. - person autistic; 16.05.2015

Если вы работаете в системе POSIX, такой как Linux, у вас должен быть доступ к getline. Его можно заставить вести себя как fgets, но если вы начнете с нулевого указателя и нулевой длины, он позаботится о выделении памяти за вас.

Вы можете использовать in в цикле следующим образом:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>    // for strcmp

int main(void)
{
    char *line = NULL;
    size_t nline = 0;

    for (;;) {
        ptrdiff_t n;

        printf("> ");

        // read line, allocating as necessary
        n = getline(&line, &nline, stdin);
        if (n < 0) break;

        // remove trailing newline
        if (n && line[n - 1] == '\n') line[n - 1] = '\0';

        // do stuff
        printf("'%s'\n", line);
        if (strcmp("quit", line) == 0) break;
    }

    free(line);
    printf("\nBye\n");

    return 0;
}

Переданный указатель и значение длины должны быть согласованы, чтобы getline мог перераспределять память по мере необходимости. (Это означает, что вы не должны изменять nline или указатель line в цикле.) Если строка подходит, один и тот же буфер используется при каждом проходе цикла, так что вам придется free строку строки только один раз, когда вы Закончили читать.

person M Oehm    schedule 16.05.2015

Некоторые упомянули, что scanf, вероятно, не подходит для этой цели. Я бы тоже не советовал использовать fgets. Хотя он немного более удобен, есть проблемы, которых сложно избежать, по крайней мере на первый взгляд. Немногим программистам на C удается правильно использовать fgets с первого раза, не прочитав руководство по fgets в полном объеме. Части, которыми большинство людей полностью пренебрегают:

  • что происходит, когда линия слишком велика, и
  • что происходит, когда возникает EOF или ошибка.

Функция fgets() должна считывать байты из stream в массив, на который указывает s, до тех пор, пока не будет прочитано n-1 байтов, или не будет прочитано и передано s, или не будет обнаружено условие конца файла. Затем строка завершается нулевым байтом.

После успешного завершения fgets() возвращает s. Если поток находится в конце файла, должен быть установлен индикатор конца файла для потока, и fgets() должен возвращать нулевой указатель. Если возникает ошибка чтения, должен быть установлен индикатор ошибки для потока, fgets() должен возвращать нулевой указатель...

Я не чувствую необходимости слишком сильно подчеркивать важность проверки возвращаемого значения, поэтому я не буду упоминать об этом снова. Достаточно сказать, что если ваша программа не проверяет возвращаемое значение, ваша программа не будет знать, когда произойдет EOF или произойдет ошибка; ваша программа, вероятно, застрянет в бесконечном цикле.

Когда '\n' отсутствует, остальные байты строки еще не прочитаны. Таким образом, fgets всегда будет анализировать строку хотя бы один раз внутри. Когда вы вводите дополнительную логику для проверки '\n', вы анализируете данные во второй раз.

Это позволяет вам realloc хранить и снова вызывать fgets, если вы хотите динамически изменить размер хранилища или отбросить оставшуюся часть строки (предупредить пользователя об усечении - хорошая идея), возможно, используя что-то вроде fscanf(file, "%*[^\n]");.

Hugomg упомянул об использовании умножения в коде динамического изменения размера, чтобы избежать квадратичных проблем во время выполнения. В этом направлении было бы неплохо избегать многократного анализа одних и тех же данных на каждой итерации (таким образом создавая дополнительные квадратичные проблемы во время выполнения). Этого можно добиться, сохранив где-нибудь количество прочитанных (и проанализированных) байтов. Например:

char *get_dynamic_line(FILE *f) {
    size_t bytes_read = 0;
    char *bytes = NULL, *temp;
    do {
         size_t alloc_size = bytes_read * 2 + 1;
         temp = realloc(bytes, alloc_size);
         if (temp == NULL) {
             free(bytes);
             return NULL;
         }
         bytes = temp;
         temp = fgets(bytes + bytes_read, alloc_size - bytes_read, f); /* Parsing data the first time  */
         bytes_read += strcspn(bytes + bytes_read, "\n");              /* Parsing data the second time */
    } while (temp && bytes[bytes_read] != '\n');
    bytes[bytes_read] = '\0';
    return bytes;
}

Те, кому удастся прочитать руководство и придумать что-то правильное (например, это), могут вскоре понять, что сложность решения fgets как минимум в два раза хуже, чем такое же решение с использованием fgetc. Мы можем избежать повторного анализа данных, используя fgetc, поэтому использование fgetc может показаться наиболее подходящим. Увы, большинству программистов на C также удается использовать fgetc неправильно, пренебрегая руководством fgetc. .

Самая важная деталь — понять, что fgetc возвращает int, а не char. Обычно он может возвращать одно из 256 различных значений от 0 до UCHAR_MAX (включительно). В противном случае он может вернуть EOF, что означает обычно существует 257 различных значений, которые fgetc (или, следовательно, getchar) может вернуть. Попытка сохранить эти значения в char или unsigned char приводит к потере информации, особенно режимов ошибок. (Конечно, это типичное значение 257 изменится, если CHAR_BIT больше 8 и, следовательно, UCHAR_MAX больше 255)

char *get_dynamic_line(FILE *f) {
    size_t bytes_read = 0;
    char *bytes = NULL;
    do {
         if ((bytes_read & (bytes_read + 1)) == 0) {
             void *temp = realloc(bytes, bytes_read * 2 + 1);
             if (temp == NULL) {
                 free(bytes);
                 return NULL;
             }
             bytes = temp;
         }

         int c = fgetc(f);
         bytes[bytes_read] = c >= 0 && c != '\n'
                             ? c
                             : '\0';
    } while (bytes[bytes_read++]);
    return bytes;
}
person autistic    schedule 16.05.2015