Динамично разпределяне на въведен от потребителя низ

Опитвам се да напиша функция, която прави следните неща:

  • Стартирайте входен цикъл, отпечатвайки '> ' всяка итерация.
  • Вземете всичко, въведено от потребителя (неизвестна дължина) и го прочетете в масив от знаци, динамично разпределяйки размера на масива, ако е необходимо. Въведеният от потребителя ред ще завършва със знак за нов ред.
  • Добавете нулев байт, '\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 очаква да му подадете предварително разпределен празен буфер, който той да попълни с входа за четене. Той не локализира sctring вместо вас, така че опитите ви да инициализирате 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 връща нула при грешка. - 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
добави някои примери. моля, проверете дали не съм объркал условието за грешка с една грешка. - person hugomg; 16.05.2015
comment
@MOehm Да, прав си, не знам откъде го взех. - person ozdrgnaDiies; 16.05.2015
comment
@ozdrgnaDiies: Вероятно от непоследователна обработка на низове в C lib. В 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 байта, или a е прочетено и прехвърлено към 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