C · Intermediate

Strings in C

9 min readUpdated September 24, 2026Every example verified

In short: A C string is an array of char that ends with a zero byte, '\0'; the language has no separate string type. You read a line with fgets, measure with strlen, copy with strcpy or snprintf, join with strcat and compare with strcmp, and you must always leave room in the array for the text plus its terminator.

Text is an array with a terminator

C has no string type. Text is stored in an ordinary array of char, and the end of the text is marked by a byte whose value is zero, written '\0' and called the null terminator. The literal "kiln" therefore occupies five bytes, not four, and char label[16] = "kiln" fills the first five bytes and zeroes the rest. Nothing records the length anywhere: strlen, printf with %s and every other string function simply walk forward until they meet the zero byte. That one convention explains most of what is unusual about C strings.

Because a string is an array, it behaves like one. You cannot assign to it after declaration (label = "x" does not compile), you cannot compare two of them with == (that compares addresses, which differ for any two distinct arrays), and it does not grow. Copying, joining and comparing are done by functions from <string.h>: strcpy copies including the terminator, strcat appends, strcmp compares character by character and returns 0 when the two are equal, strchr and strstr search. snprintf builds formatted text into a buffer and, unlike strcpy and strcat, takes the buffer size, so it truncates instead of overflowing.

The array must be large enough for the text plus the terminator; the functions do not check. Writing six bytes into char code[5] overwrites whatever sits after the array, which is undefined behaviour and the root of many security bugs. Size arrays with a margin, prefer snprintf when the length is not obvious, and read input with fgets(buffer, sizeof buffer, stdin), which stops one byte before the end. fgets keeps the newline the user typed; buffer[strcspn(buffer, "\n")] = '\0' removes it in one line by finding the first newline (or the terminator, if there is no newline) and cutting there.

Two declarations look alike but differ. char a[] = "text" creates a writable array holding a copy of the literal. char *p = "text" makes p point at the literal itself, which lives in read-only storage; writing through p is undefined behaviour. Use an array when you intend to change the text and const char * when you only read it.

Individual characters are small integers: 'A' is 65 in ASCII, so c - '0' turns a digit character into its value. <ctype.h> provides toupper, isdigit and friends; pass them a char cast to unsigned char, because a negative char is not a valid argument.

Syntax

 C · syntax
char name[32] = "text";        /* array: writable, holds at most 31 characters */
const char *label = "text";    /* pointer to a read-only literal */

fgets(name, sizeof name, stdin);         /* read a line, at most 31 chars */
name[strcspn(name, "\n")] = '\0';        /* drop the newline that fgets keeps */

strlen(name)                   /* characters before the terminator */
strcpy(dest, src);             /* copy; dest must have room */
strcat(dest, src);             /* append to the text already in dest */
strcmp(a, b)                   /* 0 if equal, negative if a sorts first, positive if b does */
strchr(s, 'c');  strstr(s, "sub");       /* pointer to the match, or NULL */
snprintf(buf, sizeof buf, "%s-%d", name, n);   /* build text without overflowing */

sizeof name is the whole array (32 here); strlen(name) is the length of the text in it. Every function that writes into an array needs the array to have room for the result plus one terminator byte.

The string.h functions you will use most

FunctionWhat it doesWatch out for
strlen(s)number of characters before the terminatorreturns size_t; print it with %zu
strcpy(d, s)copies s into d, terminator includedd needs strlen(s) + 1 bytes
strncpy(d, s, n)copies at most n bytesadds no terminator when s has n or more characters
strcat(d, s)appends s to the text already in dd needs room for both texts plus 1
strcmp(a, b)0 when equal, negative or positive otherwiseonly the sign is meaningful
strchr(s, c)pointer to the first c in s, or NULLtest for NULL before using the result
strstr(s, sub)pointer to the first occurrence of sub, or NULLsame
strcspn(s, set)length of the prefix containing none of the characters in setthe fgets newline trick
snprintf(buf, n, fmt, ...)formats into buf, writing at most n bytes including the terminatorreturns the length it wanted, which may exceed n

The terminator, strlen and sizeof

A 16-byte array holding four characters, examined byte by byte.

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

int main(void) {
    char label[16] = "kiln";

    printf("strlen: %zu\n", strlen(label));
    printf("sizeof: %zu\n", sizeof label);
    for (int i = 0; i <= 4; i++) {
        printf("label[%d] = %d\n", i, label[i]);
    }

    label[0] = 'K';
    printf("%s\n", label);

    label[2] = '\0';
    printf("%s (strlen now %zu)\n", label, strlen(label));
    return 0;
}

Output

strlen: 4
sizeof: 16
label[0] = 107
label[1] = 105
label[2] = 108
label[3] = 110
label[4] = 0
Kiln
Ki (strlen now 2)

strlen counts 4 because it stops at the zero byte in label[4]; sizeof reports the whole 16-byte array whether or not text fills it. Printing each element with %d shows the characters as the numbers they really are, ASCII codes 107, 105, 108 and 110, followed by the 0 that ends the string. Because the array is writable, label[0] = 'K' changes one character in place. Writing a terminator into label[2] deletes nothing: l and n are still there, but every string function now stops after Ki.

Reading a line with fgets and cleaning it up

The input is the single line granary loaf.

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

int main(void) {
    char line[64];
    if (fgets(line, sizeof line, stdin) == NULL) {
        return 1;
    }
    printf("raw length: %zu\n", strlen(line));

    line[strcspn(line, "\n")] = '\0';
    printf("[%s] length %zu\n", line, strlen(line));

    char upper[64];
    strcpy(upper, line);
    for (size_t i = 0; upper[i] != '\0'; i++) {
        upper[i] = (char) toupper((unsigned char) upper[i]);
    }
    printf("%s\n", upper);

    int words = 1;
    for (size_t i = 0; line[i] != '\0'; i++) {
        if (line[i] == ' ') {
            words++;
        }
    }
    printf("words: %d\n", words);
    return 0;
}

Input given to the program: granary loaf

Output

raw length: 13
[granary loaf] length 12
GRANARY LOAF
words: 2

fgets reads up to 63 characters or to the end of the line, and it keeps the newline, which is why the raw length is 13 for 12 visible characters. The strcspn line finds the position of the first newline and overwrites it with a terminator. strcpy makes an independent copy in upper, and toupper converts one character at a time; the cast to unsigned char is the correct way to hand a char to the <ctype.h> functions. The word-count loop is the standard pattern for scanning a string: advance until the terminator, examining each character on the way.

Building, comparing and searching

Assembling a file path from pieces, then asking questions about it.

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

int main(void) {
    char path[64];
    strcpy(path, "reports/");
    strcat(path, "week-38");
    strcat(path, ".csv");
    printf("%s\n", path);

    char alt[64];
    snprintf(alt, sizeof alt, "reports/week-%d.csv", 39);
    printf("%s\n", alt);

    int cmp = strcmp(path, alt);
    printf("path %s alt\n", cmp < 0 ? "sorts before" : cmp > 0 ? "sorts after" : "equals");
    printf("equal strings give %d\n", strcmp("week", "week"));

    const char *ext = strstr(path, ".csv");
    printf("extension starts at index %d\n", (int) (ext - path));

    const char *slash = strchr(path, '/');
    printf("directory part is %d characters\n", (int) (slash - path));
    printf("after the slash: %s\n", slash + 1);
    return 0;
}

Output

reports/week-38.csv
reports/week-39.csv
path sorts before alt
equal strings give 0
extension starts at index 15
directory part is 7 characters
after the slash: week-38.csv

strcpy puts the first piece into path, and each strcat appends to whatever is already there; the 64-byte array has plenty of room. snprintf does the same job in one call and is the safer habit because it takes the buffer size. strcmp walks both strings until they differ: at index 14 path has 8 and alt has 9, so path sorts first and the result is negative. Only the sign is specified, so compare the result with 0 rather than with a particular number. strstr and strchr return pointers into the string; subtracting the start of the array gives an index, and slash + 1 is itself a valid string starting one character later (see the pointers lesson).

Common mistakes

  • Comparing strings with ==

    Why it goes wrong: if (answer == "yes") compares two addresses, the array and the literal, which are never the same, so the test is always false and the compiler often says nothing.

    Fix: Use strcmp(answer, "yes") == 0.

     C · fix
    if (strcmp(answer, "yes") == 0) {
        printf("confirmed\n");
    }
  • Leaving no room for the terminator

    Why it goes wrong: char code[3] = "ABC" is legal in C but stores no zero byte, so printf("%s", code) reads past the array until it happens to find one.

    Fix: Declare the array one larger than the longest text it must hold, or let the compiler size it with char code[] = "ABC".

     C · fix
    char code[4] = "ABC";        /* 3 letters + terminator */
    char auto_sized[] = "ABC";   /* also 4 bytes */
  • Reading input with an unbounded %s

    Why it goes wrong: scanf("%s", buf) writes as many characters as the user types. A word longer than the array overwrites neighbouring memory.

    Fix: Prefer fgets(buf, sizeof buf, stdin), or give scanf a width one less than the array size: scanf("%31s", buf) for a 32-byte array.

  • Modifying a string literal through a pointer

    Why it goes wrong: char *msg = "draft"; msg[0] = 'D'; writes to read-only memory. It is undefined behaviour and on most systems crashes.

    Fix: Use an array when you need to edit: char msg[] = "draft"; msg[0] = 'D';.

Where you use this

Almost every program that talks to a person parses text. A configuration line such as port=8080 is split by finding the = with strchr, terminating the key there, and converting the remainder with strtol. A command typed at a prompt is matched against the known commands with strcmp, and output paths and log lines are built with snprintf. The same habits recur in all of them: read with a bounded function, cut the newline, and check every pointer a search function returns for NULL before using it.

 C · in practice
char line[64] = "port=8080";
char *eq = strchr(line, '=');
if (eq != NULL) {
    *eq = '\0';                              /* line is now "port" */
    long value = strtol(eq + 1, NULL, 10);   /* 8080 */
}

Key points

  • A string is a char array ending in a zero byte; the array must be at least one byte longer than the text.
  • strlen measures the text; sizeof measures the array.
  • Assign and compare with strcpy or snprintf and strcmp, never with = and ==.
  • Read lines with fgets and remove the newline with strcspn.
  • char a[] = "..." is writable; char *p = "..." points at a read-only literal.
  • Search functions return a pointer or NULL; test it before dereferencing.

Try it yourself

Read one word and print it reversed. The starter already reads the line and removes the newline; add a loop that walks from the last character to the first using strlen. For the input stream the program prints maerts.

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

int main(void) {
    char word[64];
    if (fgets(word, sizeof word, stdin) == NULL) {
        return 1;
    }
    word[strcspn(word, "\n")] = '\0';

    /* print the characters of word from last to first, then a newline */

    return 0;
}
Input the program receives: stream
Expected output: maerts

Practise this

Exercises for this lesson are in the C practice set.

Open the C playground

Frequently asked questions

Why does fgets leave a newline at the end of the string?

Because it reads a whole line including its terminating newline, so that the caller can tell whether the line was complete or cut off by the buffer size: if the string ends in a newline the whole line fitted, otherwise the line was longer than the buffer. Remove it with s[strcspn(s, "\n")] = '\0', which is safe even when there is no newline.

What is the difference between strlen and sizeof for a string?

strlen(s) counts characters up to the first zero byte and is computed at run time. sizeof s is the number of bytes the array occupies, fixed at compile time, and includes the terminator and any unused space. For char s[16] = "kiln" they are 4 and 16. Applied to a pointer, sizeof gives the size of the pointer rather than the text, which is why a string passed to a function must be measured with strlen.

Is strcpy safe to use in C?

Only when you know the destination has room for the source plus the terminator; strcpy itself checks nothing. When the source comes from input or its length is not obvious, use snprintf(dest, sizeof dest, "%s", src), which truncates instead of overflowing, or test strlen(src) < sizeof dest first.

Progress is stored only in this browser.

How this page was checked. Every program on it was run with GCC 13 (C11) at build time by the publishing checks, and the output shown is what it printed. Running C inside the browser is not available yet, so the Run button is absent rather than pretending; copy the code and run it with GCC 13 (C11) locally.