4
\$\begingroup\$

From scanf() to fgets(), we need to first specify the maximum length of the string input, due to which input gets a limitation, which isn't that good. As input can vary from [0, ∞), due to computer limitation input will never reach ∞, but it can be very large.

I have seen in C++, there no limit for string input.

std::string x;
std::cin >> x;

So, to implement this feature in C, I have created a function which takes a single character from the user and appends it at the last of the buffer, after that it calls realloc() for the buffer.

Final Code

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

static inline void exit_heap_fail(const void *ptr)
{
    if (!ptr)
    {
        fprintf(stderr, "err: null-pointer\n");
        exit(EXIT_FAILURE);
    }
}

static char *input(void)
{
    char *ptr = calloc(2, sizeof(char)), ch;
    exit_heap_fail(ptr);
    size_t len = 0;
    while ((ch = getchar()))
    {
        if (ch == 10 || ch == 0)
            break;
        ptr[len++] = ch;
        ptr = realloc(ptr, len + 1);
        exit_heap_fail(ptr);
    }
    ptr[len] = 0;
    return ptr;
}

int main(void)
{
    puts("Enter:");
    char *data = input();
    printf("DATA = `%s`\n", data);
    free(data);
    return EXIT_SUCCESS;
}
\$\endgroup\$
7
  • \$\begingroup\$ Are you aware of getline() ? \$\endgroup\$
    – Martin R
    Apr 30, 2022 at 6:19
  • 2
    \$\begingroup\$ Note that getline() is not part of the Standard C library although its functional design is worth reviewing as a guide here. \$\endgroup\$ Apr 30, 2022 at 13:12
  • \$\begingroup\$ getline() is a part of the Dynamic Allocation Functions TR and POSIX too, however \$\endgroup\$
    – JohnScott
    Apr 30, 2022 at 22:54
  • \$\begingroup\$ As you note std::string is also limited by the computer's memory. \$\endgroup\$
    – qwr
    May 1, 2022 at 8:30
  • \$\begingroup\$ @qwr But it can be appended, and its limit isn't assigned by the programmer \$\endgroup\$ May 1, 2022 at 8:39

1 Answer 1

6
\$\begingroup\$

End-of-file

When nothing is read, code returns an allocated "". That is indistinguishable from reading only a '\n' as that returns "" too.

To function like other standard input functions, code should return some indication, like NULL:

  • No input and end-of-file reported.

  • Input error occurs, regardless is anything read.

Infinite loop

When end-of-file occurs getchar() returns EOF and code never exits the loop.

// Infinite loop
while ((ch = getchar())) {
    if (ch == 10 || ch == 0)
        break;

Redundant test

Code test for the null character twice.

257

getchar() returns 257 different values (EOF and all unsigned char). Saving that in a char loses information.

Use int ch.

Repetitive calls to re-allocate

realloc(ptr, len + 1); calls realloc() every iteration. A more efficient common approach approximately doubles the size each loop with a right-size allocation in the end.

Ending input on a null character

This is different from fgets(), scanf().


[Edit]

Some sample code to illustrate these ideas and a few more. Only lightly tested.
Header:

/* 
 * Allocate as needed to form a string from user input.
 *
 * Return NULL on
 * * End of file with no input
 * * Input error
 * * Out of memory
 * Caller to use feof(), ferror() to distinguish.
 *
 * Otherwise return allocated buffer.
 * Caller to free buffer.
 */
char* line_alloc(void);

Source

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

#define LINE_ALLOC_MIN 63  /* Some power-of-2 minus 1 */

static char* line_alloc_helper(char *buf, size_t *len_max_ptr) {
  // Is input longer than supportable size?
  if (*len_max_ptr > SIZE_MAX / 2 - 1) {
    free(buf);
    return NULL;
  }
  size_t sz = *len_max_ptr + 1;  // Old buffer size
  sz = 2 * sz + 1;               // New buffer size
  if (sz < LINE_ALLOC_MIN) {
    sz = LINE_ALLOC_MIN;
  }
  char *buf_new = realloc(buf, sz);
  if (buf_new == NULL) {
    free(buf);
    return NULL;
  }
  *len_max_ptr = sz - 1;
  return buf_new;
}

char* line_alloc(void) {
  size_t len = 0;
  size_t len_max = 0;
  char *buf = NULL;
  int ch;
  bool nothing_read = true;

  while ((ch = getchar()) != EOF) {
    nothing_read = false;
    if (ch == '\n' || ch == '\0') { // Remove ch == '\0' if desired.
      break;
    }
    if (len >= len_max) {
      buf = line_alloc_helper(buf, &len_max);
      if (buf == NULL) {
        return NULL;
      }
    }
    buf[len++] = (char) ch;
  }

  if (ch == EOF) {
    if (nothing_read || !feof(stdin)) {  // Note 1
      free(buf);
      return NULL;
    }
  }

  char *buf_right_size = realloc(buf, len + 1);
  if (buf_right_size == NULL) {
    free(buf);
  } else {
    buf_right_size[len] = '\0';
  }
  return buf_right_size;
}

/* Note 1: Better as !feof() than ferror() to identify 
end-of-file with input error set from prior activity. */

Test:

#include <string.h>

int main(void){
    char * buf;
    while ((buf = line_alloc()) != NULL) {
        size_t len = strlen(buf);
        printf("%2zu <%s>\n", len, buf);
        free(buf);
        fflush(stdout);
    }
}

Future improvements/ideas:

  • Support FILE *input_stream.

  • Might as well use local buffer, say 127 bytes, and them malloc() during the right-size step for short lines. Transition to allocated buffers for long lines. Adds complexity, but makes for only one allocation for common lines.

  • Cope with '\r' '\n' pairs that come up from reading foreign text files.

  • Provide caller with len to better support lines with embedded null characters.

  • Provide an upper bound rather than SIZE_MAX for max line length to help prevent hacker abuse of code.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.