xSKILLby Infirals
Register

C for Beginners

0 of 24 notes read 0%

Reading progress is saved to your INFIRALS account. Sign in to keep this note marked as read.

Building strings by hand

Working with text12 min readLesson 3 of 3

3.1 Two numbers describe a buffer

Building text in C means writing bytes into an array and keeping the terminator honest. Two values make that manageable: capacity, the bytes available, and used, the bytes already written before the terminator.

The invariant is used < capacity, and buffer[used] == '\0'. Every append checks that there is room for the new text plus its terminator.

Passing both values to a helper keeps the checks in one place. The helper returns the new used count, so the caller does not repeat the arithmetic.

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

static size_t append(char *buffer, size_t capacity, size_t used, const char *text)
{
    size_t needed = strlen(text) + 1;
    if (capacity - used < needed) return used;
    memcpy(buffer + used, text, needed);
    return used + needed - 1;
}
int main(void)
{
    char buffer[32] = "";
    size_t used = 0;
    used = append(buffer, sizeof buffer, used, "Hello");
    used = append(buffer, sizeof buffer, used, ", ");
    used = append(buffer, sizeof buffer, used, "world");
    printf("%s (%zu)\n", buffer, used);
    return 0;
}

Output

Hello, world (12)

3.2 The terminator has to be copied too

A copy loop that stops before the terminator leaves a buffer that prints whatever followed it. The terminator is part of the string, so a copy loop writes one more byte than the visible characters.

When the destination is smaller than the source, the last byte must be set to '\0' explicitly. Truncation happens at capacity - 1 bytes, not at capacity.

strlen counts the bytes before the terminator, so adding one gives the size to allocate or copy.

c
#include <stdio.h>
static void copy_bounded(char *dest, size_t capacity, const char *src)
{
    size_t i = 0;
    while (i + 1 < capacity && src[i] != '\0') {
        dest[i] = src[i];
        i++;
    }
    dest[i] = '\0';
}
int main(void)
{
    char small[4], exact[6];
    copy_bounded(small, sizeof small, "abcdefgh");
    copy_bounded(exact, sizeof exact, "abcde");
    printf("[%s] %zu\n", small, sizeof small);
    printf("[%s] %zu\n", exact, sizeof exact);
    return 0;
}

Output

[abc] 4
[abcde] 6

3.3 Successive snprintf calls

snprintf can write into a buffer at an offset, which makes it a building block for formatted text. Each call gets the remaining capacity and returns how much of it the full output would need.

The return value can exceed the remaining capacity. Clamp it before adding: if the return is greater than or equal to the room, the buffer is full and the used count stops at capacity - 1.

This pattern handles mixed content, such as a label, a number, and punctuation, without manual number formatting.

c
#include <stdio.h>

int main(void)
{
    char record[48];
    int used = 0;
    int capacity = (int)sizeof record;

    used += snprintf(record + used, (size_t)(capacity - used), "id=%d", 7);
    used += snprintf(record + used, (size_t)(capacity - used), ", name=%s", "Ada");
    used += snprintf(record + used, (size_t)(capacity - used), ", score=%d", 91);
    printf("%s (%d bytes)\n", record, used);
    return 0;
}

Output

id=7, name=Ada, score=91 (24 bytes)

3.4 Joining words with a separator

The separator belongs between items, not after the last one. A loop that adds ", " before every item except the first keeps the output clean and avoids a trailing comma.

That conditional is the whole trick, and it works for any number of items. Starting the used count at zero also handles an empty list with no special case.

With a capacity check inside the loop, a long list stops at an item boundary instead of being cut in the middle of a word.

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

int main(void)
{
    const char *items[] = {"salt", "pepper", "cumin"};
    char buffer[64] = "";
    size_t used = 0;

    for (int i = 0; i < 3; i++) {
        const char *sep = (i == 0) ? "" : ", ";
        int written = snprintf(buffer + used, sizeof buffer - used, "%s%s", sep, items[i]);
        if (written < 0 || (size_t)written >= sizeof buffer - used) break;
        used += (size_t)written;
    }
    printf("%s (%zu)\n", buffer, used);
    return 0;
}

Output

salt, pepper, cumin (19)

3.5 An integer to text without printf

Formatting a number by hand shows what %d does internally. Take the last digit with % 10, convert it to a character, then divide by 10 and repeat.

Digits come out in reverse order, so the loop writes into a temporary array backwards or reverses the result afterwards. This example writes the digits from the end of a small buffer toward the start.

The do loop runs once for zero, which a while (value != 0) loop would skip. Negative numbers need a sign and an unsigned magnitude, because negating INT_MIN overflows.

c
#include <stdio.h>

static void format_unsigned(char *out, unsigned int value)
{
    char digits[12];
    int count = 0;
    do {
        digits[count++] = (char)('0' + value % 10);
        value /= 10;
    } while (value != 0);
    for (int i = 0; i < count; i++) out[i] = digits[count - 1 - i];
    out[count] = '\0';
}
int main(void)
{
    char text[16];
    format_unsigned(text, 9042);
    printf("%s\n", text);
    return 0;
}

Output

9042

3.6 Where manual building goes wrong

  • forgetting the terminator after a copy loop
  • checking used < capacity when the new text needs used + length + 1
  • subtracting a larger size from a smaller one in size_t, which wraps to a huge number
  • calling strlen on the whole buffer inside a loop, which turns a linear build into a quadratic one
  • treating the return of snprintf as the number of bytes actually written

The unsigned subtraction bug is the hardest to spot: capacity - used underflows when used has already passed capacity, and the check that follows approves an enormous write.

Guard the subtraction with a comparison before it, or keep a single helper that owns every write into the buffer.

c
#include <stdio.h>

int main(void)
{
    size_t capacity = 10;
    size_t used = 14;             /* pretend a bug let this happen */
    size_t room = (used > capacity) ? 0 : capacity - used;

    printf("room: %zu\n", room);
    printf("guard needed: %d\n", used > capacity);
    return 0;
}

Output

room: 0
guard needed: 1

3.7 A small builder worth keeping

A struct Text that holds a pointer, a capacity, and a used count turns these rules into one object. Appending becomes a function that returns a status, and the caller stops tracking offsets.

That structure appears when records are read from files. Keeping the terminator and the count in agreement is easier when only two functions ever write to the buffer.

For fixed buffers, a plain array and a used count are enough. The struct becomes useful when the buffer is heap allocated and its capacity can change.

Interview questions

3 questions from this note. Open one to see the answer; no account needed.

  • 01What invariant should a string builder keep?

    The used count stays below the capacity, and `buffer[used]` is always `'\0'`. Every write checks that the new text plus the terminator fits.

  • 02Why can `snprintf` return a number larger than the space left?

    It returns the length the complete output would need, not the number of bytes written. That is how the caller detects truncation.

  • 03Why convert to `unsigned` when formatting a negative number by hand?

    Negating `INT_MIN` overflows a signed `int`. Working with the unsigned magnitude avoids undefined behaviour.