Skip to main content
C beginner Lesson 9 of 23

Strings in C

Learn how C strings work as null-terminated char arrays, how to use string.h functions, and how to handle strings safely.

How C Strings Work

C has no built-in string type. A string is simply an array of char values terminated by a null byte ('\0', value 0). Every string function in the standard library relies on this convention — they walk the array one byte at a time until they find the '\0'. This design is minimal and efficient, but it puts the burden on you to ensure null termination and sufficient buffer sizes.

#include <stdio.h>

int main(void) {
    /* String literal — stored in read-only data segment, cannot be modified */
    const char *greeting = "Hello";

    /* char array — writable copy on the stack, null terminator included automatically */
    char name[] = "Alice";   /* name has 6 chars: A l i c e \0 */

    /* Manual construction — you must add the null terminator yourself */
    char word[6];
    word[0] = 'H';
    word[1] = 'e';
    word[2] = 'l';
    word[3] = 'l';
    word[4] = 'o';
    word[5] = '\0';   /* MUST terminate — without this, it is not a valid C string */

    printf("%s\n", greeting);
    printf("%s has %zu characters\n", name, sizeof(name) - 1);

    /* Show individual bytes — note sizeof includes the null terminator */
    for (int i = 0; name[i] != '\0'; i++) {
        printf("name[%d] = '%c' (%d)\n", i, name[i], (unsigned char)name[i]);
    }

    return 0;
}

sizeof(name) gives 6 (including '\0'). strlen(name) gives 5 (not including '\0'). Always allocate length + 1 bytes to hold a string of known length.

The <string.h> Functions

The standard library provides a rich set of string functions. Knowing them well means you don’t reinvent the wheel, but each one has gotchas — particularly around buffer sizes and null termination.

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

int main(void) {
    char src[] = "Hello, World!";
    char dst[64];

    /* strlen — character count, not including the null terminator */
    printf("Length: %zu\n", strlen(src));   /* 13 */

    /* strcpy — copy string (UNSAFE if dst is too small) */
    strcpy(dst, src);
    printf("Copy: %s\n", dst);

    /* strncpy — copy at most n bytes; may NOT null-terminate if src is too long */
    char limited[8];
    strncpy(limited, src, sizeof(limited) - 1);
    limited[sizeof(limited) - 1] = '\0';   /* always add null terminator manually */
    printf("Limited: %s\n", limited);       /* Hello, */

    /* strcat — append (UNSAFE if dst lacks space) */
    char buf[64] = "Hello";
    strcat(buf, ", World");
    printf("Concat: %s\n", buf);

    /* strncat — safe append: at most n bytes + null terminator */
    char safe[16] = "Hello";
    strncat(safe, ", World!!!", sizeof(safe) - strlen(safe) - 1);
    printf("Safe concat: %s\n", safe);

    /* strcmp — compare; returns 0 if equal, <0 if first < second, >0 if first > second */
    printf("strcmp: %d\n", strcmp("apple", "banana"));   /* negative */
    printf("strcmp: %d\n", strcmp("abc", "abc"));        /* 0 */
    printf("strcmp: %d\n", strcmp("zebra", "ant"));      /* positive */

    /* strncmp — compare at most n characters */
    printf("%d\n", strncmp("hello world", "hello there", 5));  /* 0 — first 5 chars match */

    /* strchr — find first occurrence of a character, returns pointer or NULL */
    char *pos = strchr(src, 'W');
    if (pos) printf("Found 'W' at index %td\n", pos - src);  /* 7 */

    /* strrchr — find last occurrence */
    char *last_l = strrchr(src, 'l');
    printf("Last 'l' at index %td\n", last_l - src);   /* 10 */

    /* strstr — find substring */
    char *sub = strstr(src, "World");
    if (sub) printf("Found 'World': %s\n", sub);

    /* strtok — tokenize (MODIFIES the string — use a copy if you need the original) */
    char csv[] = "one,two,three,four";
    char *token = strtok(csv, ",");
    while (token != NULL) {
        printf("Token: %s\n", token);
        token = strtok(NULL, ",");  /* pass NULL to continue from where we left off */
    }

    return 0;
}

Safe String Handling with snprintf

The safest way to build strings in C is snprintf. Unlike sprintf, it always null-terminates and never writes more than the specified number of bytes — making buffer overflows impossible. It also returns the number of characters that would have been written, so you can detect truncation.

#include <stdio.h>

int main(void) {
    char buf[64];
    const char *first = "John";
    const char *last  = "Doe";
    int age = 30;

    int written = snprintf(buf, sizeof(buf), "%s %s, age %d", first, last, age);

    if (written < 0) {
        fprintf(stderr, "Encoding error\n");
    } else if ((size_t)written >= sizeof(buf)) {
        fprintf(stderr, "Output was truncated\n");
    } else {
        printf("%s\n", buf);   /* John Doe, age 30 */
    }

    return 0;
}

snprintf returns the number of characters that would have been written (not counting '\0'). If the return value is >= the buffer size, the output was truncated.

Converting Strings to Numbers

C provides several functions for parsing numbers from strings. atoi is the simplest but provides no error detection. strtol and strtod are the preferred choices because they report exactly where parsing stopped and whether an overflow occurred.

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

int main(void) {
    /* atoi — simple but no error checking: returns 0 for both "0" and invalid input */
    int n = atoi("42");
    printf("%d\n", n);     /* 42 */
    printf("%d\n", atoi("abc"));  /* 0 — but was it intentional? You can't tell */

    /* strtol — preferred: detects errors, reports where parsing stopped, handles bases */
    char *endptr;
    errno = 0;
    long val = strtol("  -123abc", &endptr, 10);

    if (errno != 0) {
        perror("strtol");
    } else if (endptr == "  -123abc") {
        fprintf(stderr, "No digits found\n");
    } else {
        printf("Parsed: %ld, stopped at: '%s'\n", val, endptr);
        /* Parsed: -123, stopped at: 'abc' */
    }

    /* strtod for floating-point */
    double f = strtod("3.14xyz", &endptr);
    printf("Float: %.2f, remaining: '%s'\n", f, endptr);  /* 3.14, 'xyz' */

    /* strtol handles different bases: hex, octal, binary */
    printf("%ld\n", strtol("0xFF", NULL, 16));  /* 255 */
    printf("%ld\n", strtol("0777", NULL, 8));   /* 511 */
    printf("%ld\n", strtol("0xFF", NULL, 0));   /* 255 — auto-detect base from prefix */

    return 0;
}

String Building Patterns

When you need to assemble a string from multiple parts, calculate the required length first, allocate the buffer, then fill it. This avoids both truncation and overflow.

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

/* Join an array of strings with a separator; caller must free() the result */
char *join_strings(const char **parts, int count, const char *sep) {
    /* Calculate total length: all parts + separators + null terminator */
    size_t total = 1;
    size_t seplen = strlen(sep);
    for (int i = 0; i < count; i++) {
        total += strlen(parts[i]);
        if (i < count - 1) total += seplen;
    }

    char *result = malloc(total);
    if (!result) return NULL;

    result[0] = '\0';
    for (int i = 0; i < count; i++) {
        strcat(result, parts[i]);
        if (i < count - 1) strcat(result, sep);
    }

    return result;  /* caller must free() */
}

int main(void) {
    const char *words[] = {"the", "quick", "brown", "fox"};
    char *sentence = join_strings(words, 4, " ");
    if (sentence) {
        printf("%s\n", sentence);   /* the quick brown fox */
        free(sentence);
    }
    return 0;
}

Common String Mistakes

These mistakes are so frequent they deserve their own section. Each one is either undefined behavior or a security vulnerability.

/* MISTAKE 1: Forgetting the null terminator */
char bad[5] = {'H','e','l','l','o'};   /* not a valid C string — no '\0' */
char good[6] = {'H','e','l','l','o','\0'};  /* correct */

/* MISTAKE 2: Modifying a string literal — undefined behavior */
char *literal = "hello";
literal[0] = 'H';   /* crash — string literals are in read-only memory */
char arr[] = "hello";
arr[0] = 'H';   /* OK — arr is a writable copy on the stack */

/* MISTAKE 3: Returning a pointer to a local array — dangling pointer */
const char *bad_return(void) {
    char local[] = "temporary";
    return local;   /* local is destroyed when function returns — caller gets garbage */
}

/* MISTAKE 4: Off-by-one in buffer size — buffer overflow */
char buf[5];
strcpy(buf, "Hello");   /* "Hello" needs 6 bytes (H e l l o \0) — overflow! */
char buf2[6];
strcpy(buf2, "Hello");  /* correct */

Frequently Asked Questions

Why are C strings null-terminated?
C has no built-in string type. A string is just an array of chars where a '\0' byte marks the end. Functions like printf and strlen walk the array byte by byte until they hit '\0'.
What is the difference between a string literal and a char array?
A string literal like "hello" is stored in read-only memory — you cannot modify it. A char array like char s[] = "hello" copies the literal into a writable stack buffer that you can modify.
Why should I use strncpy instead of strcpy?
strcpy has no length limit and will overflow the destination buffer if the source is too long. strncpy limits the copy to n bytes. Even better: use snprintf or strlcpy (BSD/POSIX) for safe string copying.