You can use the snprintf function with NULL for the first argument and 0 for the second get the size that a formatted string would be.  You can then allocate the space dynamically and call snprintf again to actually build the string.
char *buffer = NULL; 
int len, offset = 0;
len = snprintf (NULL, 0, "%d plus %d is %d", 5, 3, 5+3);
buffer = realloc(buffer, offset + len + 1);
offset = sprintf (buffer + offset, "%d plus %d is %d", 5, 3, 5+3);
len = snprintf (NULL, 0, " and %d minus %d is %d", 6, 3, 6-3);
buffer = realloc(buffer, offset + len + 1);
offset += sprintf (buffer + offset, " and %d minus %d is %d", 6, 3, 6-3);
len = snprintf (NULL, 0, " even more");
buffer = realloc(buffer, offset + len + 1);
offset += sprintf (buffer + offset, " even more");
printf ("[%s]",buffer);
Note that this implementation omits checking on realloc and snprintf for brevity.  It also repeats the format string and arguments.  The following function addresses these shortcomings:
int append_buffer(char **buffer, int *offset, const char *format, ...)
{
    va_list args;
    int len;
    va_start(args, format);
    len = vsnprintf(NULL, 0, format, args);
    if (len < 0) {
        perror("vsnprintf failed");
        return 0;
    }
    va_end(args);
    char *tmp = realloc(*buffer, *offset + len + 1);
    if (!tmp) {
        perror("realloc failed");
        return 0;
    }
    *buffer = tmp;
    va_start(args, format);
    *offset = vsprintf(*buffer + *offset, format, args);
    if (len < 0) {
        perror("vsnprintf failed");
        return 0;
    }
    va_end(args);
    return 1;
}
Which you can then call like this:
char *buffer = NULL;
int offset = 0;
int rval;
rval = append_buffer(&buffer, &offset, "%d plus %d is %d", 5, 3, 5+3);
if (!rval) return 1;
rval = append_buffer(&buffer, &offset, " and %d minus %d is %d", 6, 3, 6-3);
if (!rval) return 1;
rval = append_buffer(&buffer, &offset, " even more");
if (!rval) return 1;
printf ("[%s]",buffer);
free(buffer);