Динамически запрашивать строку, не зная ее размера

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

char fname[30];
char lname[30];

printf("Type first name:\n");
scanf("%s", fname);

printf("Type last name:\n");
scanf("%s", lname); 

printf("Your name is: %s %s\n", fname, lname);

Однако меня раздражает тот факт, что мне приходится использовать больше места, чем нужно, поэтому я не хочу использовать char fname[30], а вместо этого динамически выделяю размер строки. Какие-нибудь мысли?


person ShowLove    schedule 10.08.2014    source источник
comment
Используйте malloc для выделения огромного буфера. Помимо ваших постоянных пространств для хранения, память malloced может быть освобождена после использования.   -  person Jongware    schedule 11.08.2014
comment
Спрашивать пользователя, какова длина его имени, не получится, поэтому вам понадобится массив, достаточно большой, чтобы содержать самую длинную допустимую строку. Обратите внимание, что я говорю разрешено, потому что вы должны ограничить ввод, чтобы предотвратить переполнение буфера. Позже, если вы хотите, вы можете изменить размер массива, чтобы удалить неиспользуемые элементы массива.   -  person Fiddling Bits    schedule 11.08.2014
comment
Если у вас есть к нему доступ, вы можете использовать getline(NULL, ... для выделения буфера   -  person sapi    schedule 11.08.2014
comment
Что вы имеете в виду под отсутствием потраченного впустую пространства? Такие функции, как getline, будут тратить место по крайней мере в том смысле, что выделяют больше, чем это абсолютно необходимо.   -  person mafso    schedule 11.08.2014
comment
@mafso: это именно то, о чем спрашивает ОП. Заранее выделить строку сложно, поскольку длиной может быть имя< /а>?   -  person Jongware    schedule 11.08.2014
comment
@Jongware: Да, это почти невозможно узнать заранее. Единственный способ не тратить место впустую — это выделять байт за байтом по мере необходимости. Очевидно, это не ответ, поэтому я спросил, что имеется в виду под «без потери памяти».   -  person mafso    schedule 11.08.2014
comment
@mafso getline возвращает пространство, которое ему не нужно, через realloc, поэтому нет, оно не тратится впустую. Это явно не ответ - почему бы и нет? Для меня очевидно, что можно написать функцию, которая выделяет байты по мере необходимости. Люди, которые думают, что это неприемлемо неэффективно, не думают ясно ... время не имеет значения, и, кроме того, malloc может эффективно увеличивать выделенные фрагменты.   -  person Jim Balter    schedule 11.08.2014
comment
Я обновил код в своем ответе, чтобы обрезать неиспользуемую память перед возвратом собранного ввода, как это было предложено @FiddlingBits.   -  person ericbn    schedule 11.08.2014
comment
@JimBalter: Это, по крайней мере, промежуточная трата, я считаю, что вопрос все еще немного неясен ... Что касается вашего второго пункта (вопреки моему явно не ответу): ACK.   -  person mafso    schedule 11.08.2014
comment
@mafso Промежуточные отходы, которые не продолжаются после функции getline, вообще не являются отходами; такое перемещение стоек ворот — просто отчаянная попытка не ошибиться. И если вы найдете вопрос неясным, это полностью ваша проблема. Но спасибо за АК.   -  person Jim Balter    schedule 11.08.2014


Ответы (2)


Вы можете создать функцию, которая динамически выделяет память для ввода по мере ввода пользователем, используя getchar() для чтения по одному символу за раз.

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

void* safeRealloc(void* ptr, size_t size) {
  void *newPtr = realloc(ptr, size);
  if (newPtr == NULL) { // if out of memory
    free(ptr); // the memory block at ptr is not deallocated by realloc
  }
  return newPtr;
}

char* allocFromStdin(void) {
  int size = 32; // initial str size to store input
  char* str = malloc(size*sizeof(char));
  if (str == NULL) {
    return NULL; // out of memory
  }
  char c = '\0';
  int i = 0;
  do {
    c = getchar();
    if (c == '\r' || c == '\n') {
        c = '\0'; // end str if user hits <enter>
    }
    if (i == size) {
        size *= 2; // duplicate str size
        str = safeRealloc(str, size*sizeof(char)); // and reallocate it
        if (str == NULL) {
          return NULL; // out of memory
        }
    }
    str[i++] = c;
  } while (c != '\0');
  str = safeRealloc(str, i); // trim memory to the str content size
  return str;
}

int main(void) {
  puts("Type first name:\n");
  char* fname = allocFromStdin();

  puts("Type last name:\n");
  char* lname = allocFromStdin();

  printf("Your name is: %s %s\n", fname, lname);

  free(fname); // free memory afterwards
  free(lname); // for both pointers
  return 0;
}
person ericbn    schedule 10.08.2014
comment
Вы должны realloc в конце удалить все отходы, верно? - person Fiddling Bits; 11.08.2014
comment
Да, вы должны free() после использования данных удалить отходы! - person ericbn; 11.08.2014
comment
Убедитесь, что в конце стоит завершающая цифра 0. Я думал об этом же решении. Совершенно неожиданно он возвращает правильную строку и длину, даже если я использовал Backspace (в терминале OSX). Я бы предположил, что это требует специальной обработки в коде. - person Jongware; 11.08.2014
comment
@ericbn Нет, я имею в виду realloc в конце allocFromStdin, чтобы выделенный буфер имел идеальную длину, соответствующую имени. - person Fiddling Bits; 11.08.2014
comment
Это на самом деле имеет утечку памяти, потому что когда realloc терпит неудачу (возвращает NULL), он не освобождает старую память. Это не имеет большого значения, поскольку программы обычно завершаются при нехватке памяти, но на это стоит обратить внимание. Также Fiddling Bits верен, вы должны урезать размер возвращаемой памяти до размера содержимого. - person Jim Balter; 11.08.2014

От 1_:

• Необязательный символ 'm'. Это используется со строковыми преобразованиями (%s, %c, %[) и освобождает вызывающую сторону от необходимости выделять соответствующий буфер для хранения ввода: вместо этого scanf() выделяет буфер достаточного размера и присваивает адрес этого буфера в соответствующий аргумент-указатель, который должен быть указателем на переменную char * (эту переменную не нужно инициализировать перед вызовом). Вызывающий должен впоследствии освободить(3) этот буфер, когда он больше не требуется.

однако это расширение POSIX (как отмечено fiddling_bits).

Чтобы быть переносимым, я думаю, что в вашем случае использования я бы подготовил следующую функцию:

char *alloc_answer() {
  char buf[1000];
  fgets(buf,sizeof(buf),stdin);
  size_t l = strlen(buf);
  if (buf[l-1]=='\n') buf[l]=0; // remove possible trailing '\n'
  return strdup(buf);
}

даже если это решение будет разбивать строки длиннее 1000 символов (но оно, по крайней мере, предотвращает переполнение буфера).

Полнофункциональное решение должно было бы считывать ввод фрагментами и перераспределять буфер для каждого фрагмента...

person Emanuele Paolini    schedule 10.08.2014
comment
+1 Очень чистое решение. Предложение: удалить '\n', который fgets оставляет в buf. - person Fiddling Bits; 11.08.2014
comment
fgets считывает максимум на один байт меньше, чем второй аргумент, и по-прежнему 0-завершает буфер. fgets(buf, sizeof buf, stdin); в порядке. - person mafso; 11.08.2014