The Loop Gets a Name

Last lesson you wrote a loop that walked a char array until it met the null terminator, counting as it went. That loop is so central to working with strings that the standard library ships it, along with the rest of the string operations, in a header called <string.h>. Include it and the counting loop has a name.

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

int main(void)
{
    char greeting[] = "hi";
    char word[] = "memory";

    printf("greeting: strlen %zu, sizeof %zu\n", strlen(greeting), sizeof(greeting));
    printf("word: strlen %zu, sizeof %zu\n", strlen(word), sizeof(word));

    return 0;
}
greeting: strlen 2, sizeof 3
word: strlen 6, sizeof 7

strlen is last lesson's count_characters with a better name and none of your code behind it. It takes a const char *, walks forward from that address, and returns how many characters it passed before meeting '\0'. It does the same work your loop did, which means it costs the same: strlen is a walk over the whole string, not a lookup of a stored length. Calling it in a loop condition walks the string again on every iteration, so when the string is not changing, compute it once into a variable.

Note the specifier. strlen returns size_t, not int, so print it with %zu, which is chapter 1's rule about matching the specifier to the type arriving at the first function where you are likely to forget it. %d here is a mismatch and -Wall will say so.

Then note the pairing, because it is the one thing about strlen that catches everyone once. char greeting[] = "hi"; has sizeof 3 and strlen 2, and the two numbers are printed side by side above so the gap is impossible to miss. It is last lesson's rule with the library's name on it: strlen stops at the terminator and excludes it, while sizeof on the array counts every byte and includes it. When you are sizing a buffer you want the sizeof number. When you are asking how long the text is you want strlen. Reach for the wrong one and you size the buffer one byte short, which is the n + 1 rule failing in the most ordinary way there is.

Comparing Strings

Two strings are equal when they hold the same characters, and finding that out means walking both at once. strcmp does the walking. Its return value is where the trouble starts.

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

int main(void)
{
    char first[] = "apple";
    char second[] = "banana";
    char third[] = "apple";
    const char *left = first;
    const char *right = third;

    printf("apple vs banana: %d\n", strcmp(first, second));
    printf("banana vs apple: %d\n", strcmp(second, first));
    printf("apple vs apple: %d\n", strcmp(first, third));
    printf("left == right: %d\n", left == right);

    return 0;
}
apple vs banana: -1
banana vs apple: 1
apple vs apple: 0
left == right: 0

strcmp returns a negative number if the first string sorts before the second, zero if they are equal, and a positive number if the first sorts after. Three answers, not two, which is exactly what a sorting routine needs and exactly what trips up a reader expecting yes or no. Only the sign is promised. The -1 and 1 above are this library's choice and another library may return -5, so compare the result against zero and never against 1.

That makes the classic misreading worth spelling out. if (strcmp(a, b)) looks like "if a and b compare" and does the opposite of what it looks like, since it is true precisely when the strings differ: equal strings return 0, and 0 is false. The spelling you want is if (strcmp(a, b) == 0), read aloud as "if the difference is zero". This is also where chapter 2's loose end gets tied off. switch needs integer constants to branch on, so it can never branch on strings; a chain of if (strcmp(...) == 0) is what C offers instead.

The last line of output is the trap that survives longest. == on strings does not compare characters. Both operands decay to pointers, so left == right asks whether the two pointers hold the same address, and they do not: left aims at first and right aims at third, two separate arrays that happen to hold the same six bytes. The comparison is false at the same moment strcmp reports the contents identical. Written directly on the arrays as first == third, gcc catches it with warning: comparison between two arrays [-Warray-compare]. Written through pointers, which is the shape you actually meet because a char * parameter is what every function receives, there is no warning at all and the program quietly decides two identical strings are different.

Copying Needs a Destination Size

You cannot copy a string with =. destination = source; is not a copy and is not even legal, because arrays are not assignable, and gcc stops it with error: assignment to expression with array type. Copying is a function call, and strcpy is the one everybody learns first. It is also the one everybody gets wrong. The next program is wrong on purpose.

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

int main(void)
{
    char destination[8];
    char source[] = "a string far too long";

    strcpy(destination, source);
    printf("%s\n", destination);

    return 0;
}

strcpy takes two pointers and copies bytes from the second to the first until it has copied the source's terminator. Read that once more and notice what is absent: nothing in the call tells strcpy how large the destination is, so nothing can stop it. The source is 22 bytes, the destination is 8, and it writes all 22. Here gcc can see both sizes at the call and says so:

warning: 'strcpy' writing 22 bytes into a region of size 8 [-Wstringop-overflow=]
    9 |     strcpy(destination, source);

Take that warning as a gift rather than a guarantee, because it only exists when the compiler can see both sizes. Pass the destination into a function as a char * and the size is gone, and the diagnostic with it. What does not go away is the run:

==12==ERROR: AddressSanitizer: stack-buffer-overflow on address 0xfbff9e1f0028 at pc 0xffffa0b127bc
WRITE of size 22 at 0xfbff9e1f0028 thread T0
    #0 0xffffa0b127b8 in memcpy (/usr/local/lib64/libasan.so.8+0xd27b8)
    #1 0x0000004008d8 in main /tmp/e.c:9
  This frame has 2 object(s):
    [32, 40) 'destination' (line 6) <== Memory access at offset 40 overflows this variable
    [64, 86) 'source' (line 7)
SUMMARY: AddressSanitizer: stack-buffer-overflow /tmp/e.c:9 in main

It is last lesson's report with one word changed, and it is the word that matters: WRITE. Reading past the end of a buffer gives you a wrong answer, but writing past the end puts bytes into storage that belongs to something else, which is where whole classes of security bug come from. Two details are worth naming. The size is 22, the source's full length including its terminator, so strcpy never intended to stop at 8. And the top frame says memcpy rather than strcpy, because at -O2 the compiler knew the length and replaced the call with a straight block copy; the library function you wrote is not always the function that runs.

The fix is to give the operation the size the call was missing, and there are two ways to do it.

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

int main(void)
{
    char destination[8];
    char shorter[] = "hello";
    char longer[] = "hello there";

    if (strlen(shorter) < sizeof(destination))
    {
        strcpy(destination, shorter);
        printf("strcpy wrote %s\n", destination);
    }

    int wanted = snprintf(destination, sizeof(destination), "%s", longer);
    printf("snprintf wrote %s\n", destination);

    if ((size_t) wanted >= sizeof(destination))
    {
        printf("truncated: %d characters wanted, %zu bytes available\n", wanted, sizeof(destination));
    }

    return 0;
}
strcpy wrote hello
snprintf wrote hello t
truncated: 11 characters wanted, 8 bytes available

The first way is to check before you copy. strcpy is a perfectly good function once you hold the length in your hand, and if (strlen(source) < sizeof(destination)) is that hand: strlen gives the characters, sizeof gives the bytes available, and < rather than <= reserves the byte for the terminator, which is the n + 1 rule written as a condition. Never write a bare strcpy without that check somewhere above it. Note that sizeof(destination) only reports 8 where destination is a real array in scope; inside a function that received a char * it reports the size of a pointer, so a function that copies must take the destination size as a parameter.

The second way is snprintf, and it is the one to reach for by default. You already know it as printf with a destination: it takes the buffer, the buffer's size, and then an ordinary format string, so "%s" copies a string and a longer format builds one out of several pieces. It has two properties the copying functions lack. It always writes a terminator, keeping at most size - 1 characters and ending them properly, so what lands in the buffer is always a valid string. And it returns the length it wanted, not the length it wrote, so comparing that against the buffer size tells you whether anything was lost. Above, "hello there" is 11 characters, only hello t fits in 8 bytes, and the returned 11 is what makes the truncation detectable instead of silent. Truncation is not a crime and often it is the right outcome, but it should be a decision you made rather than one you never noticed.

strncpy looks like the safe strcpy and is not, which earns it a warning of its own. strncpy does not write a terminator when the source is at least n characters long: it copies exactly n bytes and stops, and if the source ran out first it pads the rest with zeros, which is the behaviour of a fixed-width field copier rather than a string copier. Given a 20-character source and strncpy(destination, source, 8), all 8 bytes fill with characters and none of them is '\0', so the next %s or strlen walks straight off the end into last lesson's stack-buffer-overflow with a READ. If you use it, write the terminator yourself on the line after. strcat carries the same missing-size problem as strcpy and appends with no idea how much room is left; snprintf concatenates through its format string instead, which is one more reason it is the default here.

Key Takeaways

  • strlen is last lesson's counting loop with a name: it walks to the terminator, excludes it, and returns size_t, so print it with %zu. It is a walk, not a stored length, so do not call it once per loop iteration.
  • char s[] = "hi"; has sizeof 3 and strlen 2. sizeof counts every byte including the terminator; strlen stops before it. Size buffers with the first number.
  • strcmp returns negative, zero, or positive, not a boolean. Equal strings give 0, so if (strcmp(a, b)) is true when they differ; write if (strcmp(a, b) == 0). Only the sign is guaranteed.
  • == compares addresses, not characters. Two arrays holding the same text are not == to each other. gcc warns with -Warray-compare on arrays but says nothing when the comparison is between pointers.
  • strcpy copies until the source's terminator and is never told the destination's size, so an oversized source writes past the end. AddressSanitizer reports a stack-buffer-overflow with a WRITE. Use it only behind if (strlen(source) < sizeof(destination)).
  • Prefer snprintf(destination, sizeof(destination), "%s", source). It always terminates, and it returns the length it wanted, so wanted >= size detects truncation. strncpy is not a safe strcpy: it leaves the buffer unterminated when the source fills it.