1

I need to collect user input with only getchar() and malloc() to store it in a string (of which the size is unknown). I've done it before but forgot how I got it right and now I'm having a problem with the string, only printing the first letter which means my get_string function is not collecting all the chars from the stdin OR the pointer is not pointing to it OR it's just not printing correctly with printf.

char *get_string(void);

int main(void)
{
    printf("Input string: ");
    char *p = get_string();
    printf("Output string: %s\n", p);
}

char *get_string(void)
{
    int c = 0;
    char *str = NULL;
    char *buff = NULL;

    for(int i = 0; c != '\n'; i++)
    {
        if(i > 0) // skips on first iteration (no char collected yet)
        {
            if(i > 1) // skips on second iteration (1st char collected)
            {
                buff = malloc(i + 1);
                for(int j = 0; j < i - 1; j++)
                    buff[j] = str[j];
                free(str);
            }
            str = malloc(i + 1); // allocate space for string
            if(i > 1) // no need to copy string from buffer
            {
                for(int j = 0; j < i - 1; j++)
                    str[j] = buff[j];
                free(buff);
            }
            str[i - 1] = c; // place char into string
            str[i] = '\0'; // terminate string with '\0'
            printf("%s\n", str); // print contents on each iteration
        }
        c = getchar();
    }
    return (str);
}

If I run printf in the main with the returned string, nothing is printed. If I run the printf inside the loop it only prints on the first iteration (first letter).

What I get:

$ > gcc get_string.c -o get_string
$ > ./get_string
Input string: Hello World!
H
Output string:

What I expect:

$ > gcc get_string.c -o get_string
$ > ./get_string
Input string: Hello World!
H
He
Hel
Hell
Hello
...
Output string: Hello World!

Also, if you know a better (and shorter) way to approach this, please share.

8
  • I might have found an answer. I only saw this link now in one of the questions I had browsed over but if you know a shorter way of doing this, that would help too. Commented May 9, 2020 at 16:30
  • 3
    I think the problem is that you use malloc. Try using realloc. Commented May 9, 2020 at 16:32
  • 1
    Your code works with code::blocks on W10. Commented May 9, 2020 at 16:38
  • 1
    @ntruter42 First of all you don't need to copy manualy the buffer to the string. You can use strcpy Commented May 9, 2020 at 17:18
  • 1
    You can also use a linked-list to forgo the realloc; your data will be fragmented, but it might be easier. Commented May 9, 2020 at 20:26

2 Answers 2

3

You'll want to use realloc to extend the input buffer, although you won't want to do it for every individual character (it's a relatively expensive operation, and can result in the string being moved around in memory). A common trick is to double the size of the buffer as you reach the end of it, so as you read characters the buffer size goes from 16 to 32 to 64, etc., minimizing the number of realloc calls. The tradeoff is a little internal fragmentation - you may wind up storing 65 characters in a 128-character buffer. But on average, that shouldn't be too much of a problem. Here's an example:

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

#define START_SIZE 16 // some size that should handle most cases

/**
 * Get the next line from the specified input stream.  Return characters up to
 * (but not including) the next newline character or EOF.  Return the size of the
 * allocated buffer as well.  
 */
char *getline( FILE *stream, size_t *size )
{
  *size = START_SIZE;
  size_t i = 0;

  /**
   * Initial allocation, buf can store a string up to START_SIZE - 1 characters.
   * If initial allocation fails, return NULL.
   */
  char *buf = malloc( sizeof *buf * *size );
  if ( !buf )
  {
    fprintf( stderr, "Failure to allocate initial buffer\n" );
    return NULL;
  }

  /**
   * Read from the input stream until we see a newline or EOF.  Newline will
   * *not* be stored in the returned string.
   */
  for ( int c = fgetc( stream ); c != '\n' && c != EOF; c = fgetc( stream ))
  {
    /**
     * Have we hit the end of the input buffer yet (allowing for the terminator)?
     */
    if ( i + 1 == *size )
    {
      /**
       * Yes.  Double the size of the buffer using realloc.  
       * If realloc cannot satisfy the request, it will return 
       * NULL and leave the contents of buf unchanged.  Therefore,
       * we want to make sure we assign the result to
       * a temporary variable and check it, otherwise we
       * could potentially lose our reference to the
       * previously allocated memory, leading to a memory leak.
       */
      char *tmp = realloc( buf, sizeof *buf * (*size * 2));
      if ( tmp )
      {
        buf = tmp;
        *size *= 2;
      }
      else
      {
        fprintf( stderr, "Unable to extend buf, returning what we have so far\n");
        return buf;
      }
    }
    buf[i++] = c;
    buf[i] = 0; // zero terminate the string as we go
  }
  return buf;
}

int main( void )
{
  size_t bufsize;
  printf( "Gimme a string: ");
  char *str = getline( stdin, &bufsize );
  printf( "You entered: \"%s\"\n", str );
  printf( "length = %zu, buffer size = %zu\n", strlen( str ), bufsize);
  free( str );
  return 0;
}

And some sample runs:

john@marvin:~/Development/getline$ gcc -o getline -std=c11 -pedantic -Wall -Werror getline.c

john@marvin:~/Development/getline$ ./getline
Gimme a string: this
You entered: "this"
length = 4, buffer size = 16

john@marvin:~/Development/getline$ ./getline
Gimme a string: this is a test
You entered: "this is a test"
length = 14, buffer size = 16

john@marvin:~/Development/getline$ ./getline
Gimme a string: this is a test of
You entered: "this is a test of"
length = 17, buffer size = 32

john@marvin:~/Development/getline$ ./getline
Gimme a string: this is a test of the emergency broadcast system.
You entered: "this is a test of the emergency broadcast system."
length = 49, buffer size = 64

john@marvin:~/Development/getline$ ./getline
Gimme a string: this is a test of the emergency broadcast system.  in the event of an actual emergency, you would be dead by now.  
You entered: "this is a test of the emergency broadcast system.  in the event of an actual emergency, you would be dead by now.  "
length = 115, buffer size = 128
Sign up to request clarification or add additional context in comments.

2 Comments

This exponential growing is mathematically a better solution in terms of run-time.
Awesome! Would it then be a good thing to run safeRealloc(buf, strlen(buf)) from this answer only once at the end of getline() to make sure the allocated space is exactly the same as the string length?
1

I think this is what you need to do:

char *get_string( )
{
    char* buffer = (char*)malloc(sizeof(char));
    char c;
    int size = 0;

    c = getc(stdin);
    buffer[size++] = c;

    while( c != '\n')
    {
        c = getc(stdin);
        buffer = (char*)realloc(buffer, (size+1)*sizeof(char));

        if(buffer != NULL) // Check if space was re allocated
            buffer[size++] = c;
        else // If re allocation failed
            return NULL;
    }
    return buffer;
}

You first create a buffer with size 1 and read the first character from stdin. Then, white the next character isn't \n:

  1. Read the next character.

  2. Reallocate space for the next character (note that realloc can return NULL if the Reallocation fails, you have to check that).

  3. Add the current character to the buffer.

5 Comments

Will try it out. Do you perhaps know the difference between getc(stdin) and getchar()?
@ntruter42 getchar() is equivalent to getc(stdin), but you can use getc with other streams, for example you can read from files.
Cool. I replaced getc(stdin) with getchar() and it works perfectly! I was hoping not to use realloc (so that I could do it manually with free() and malloc()) but it saves a lot of lines so I guess I'll use it. Thanks!
I noticed it copies the '\n' character into the string as well because c = getc(stdin) is at the beginning of the loop. I changed the while(c != '\n') into while((c = getchar()) 1= '\n') and removed the c = getchar() at the beginning of loop.
I just want to point out here that allowing user input to grow your memory usage arbitrarily is asking for trouble in any serious application. Any approach like this should cap the maximum length of input to some well chosen value

Your Answer

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.