Search code examples
carraysstringmalloccalloc

How to save the scanf input only if there's enough space in the array? How to reallocate array to let the scanf input fits in?


#include <stdio.h>

int main() {

    char *mystring = calloc(2, sizeof(char));

    scanf("%10[^\n]s", mystring);

    printf("\nValue: %s\nSize of array: %d\nAllocated space: %d\n",
           mystring, 2 * sizeof(char), sizeof(char) * strlen(mystring));

    free(mystring);
}

Output:

$ ./"dyn_mem" 
laaaaaaaaaaa

Value: laaaaaaaaa
Size of array: 2
Allocated space: 10

This code can produce an undefined behavior if I enter in the scanf input a string bigger than array size. How can I handle this ?


Solution

  • There are multiple problems in your code:

    • mystring is initialized to point to an allocated block of 2 bytes. Technically, you should test for memory allocation failure.

    • the conversion format "%10[^\n]s" is incorrect: the trailing s should be removed, the syntax for character classes ends with the ].

    • the number 10 means store at most 10 characters and a null terminator into mystring. If more than 1 character needs to be stored, the code has undefined behavior.

    • the printf conversion specifier for size_t is %zu, not %d. If your C library is C99 compliant, use %zu, otherwise case the last 2 arguments as (int).

    • the sizes output do not correspond to the labels: the first is the allocated size, and the second is the length of the string.

    • the scanf() will fail if the file is empty or starts with a newline. You should test the return value of scanf(), which must be 1, to avoid undefined behavior in case of invalid input.

    • sizeof(char) is 1 by definition.

    There are many ways to achieve your goal:

    On systems that support it, such as linux with the GNU lib C, you could use an m prefix between the % and the [ in the scanf() conversion format and pass the address of a char * as an argument. scanf() will allocate an array with malloc() large enough to receive the converted input.

    Here is a modified version for linux:

    #include <stdio.h>
    #include <stdlib.h>
    
    int main() {
        char *mystring = NULL;
        if (scanf("%m[^\n]", &mystring) == 1) {
            printf("Value: %s\n"
                   "Length of string: %zu\n"
                   "Allocated space: %zu\n",
                   mystring, strlen(mystring), malloc_usable_size(mystring));
            free(mystring);
        }
        return 0;
    }
    

    On POSIX systems, you could use getline() that reads a line into an allocated array.

    On other systems, you would need to write a function that reads the input stream and reallocates the destination array as long as you don't get a newline or the end of file.

    A common compromise is to make an assumption about the maximum length of the input:

    #include <stdio.h>
    #include <stdlib.h>
    
    int main() {
        char buf[1024];
        if (scanf("%1023[^\n]", buf) == 1) {
            char *mystring = strdup(buf);
            if (mystring) {
                printf("Value: %s\n"
                       "Length of string: %d\n",
                       "Minimum allocated size: %d\n",
                       mystring, (int)strlen(mystring), (int)strlen(mystring) + 1);
                free(mystring);
            }
        }
        return 0;
    }
    

    You could also use fgets() to read a line from the input stream and strip the newline (if any). This approach has the advantage of not failing on empty lines.

    Here is a simple implementation of getline() that should fit your needs:

    #include <stdio.h>
    #include <stdlib.h>
    
    int my_getline(char **lineptr, size_t *n, FILE *stream) {
        char *ptr = *lineptr;
        size_t size = *n;
        size_t pos = 0;
        int c;
        while ((c = getc(stream) && c != '\n') {
            if (pos + 1 >= size) {
                /* reallocate the array increasing size by the golden ratio */
                size = size + (size / 2) + (size / 8) + 16;
                ptr = realloc(ptr);
                if (ptr == NULL) {
                    ungetc(c, stream);
                    return EOF;
                }
                *n = size;
                *lineptr = ptr;
            }
            ptr[pos++] = c;
            ptr[pos] = '\0';
        }
        return (int)pos;
    }
    
    int main() {
        char *mystring = NULL;  // must be initialized
        size_t size = 0;        // must be initialized
        int res;
    
        while ((res = my_getline(&mystring, &size, stdin)) >= 0) {
            printf("Value: %s\n"
                   "Length of string: %d\n",
                   "Allocated size: %d\n",
                   mystring, res, (int)size);
        }
        free(mystring);
        return 0;
    }