Strings & string.h

C has no built-in string type — a 'string' is really just a char array with a special sentinel marking where it ends. That single design decision explains almost everything unusual about working with text in C.

C never gained a dedicated string type — what every other language calls a "string" is, in C, just an ordinary array of char values with one extra convention layered on top: somewhere in that array sits a byte with the value 0, the null character '\0', marking where the real text stops. Every function declared in <string.h> is built entirely around that one convention — it scans forward until it meets that byte and treats everything before it as the string's content. Once that idea is solid, the rest of this page — how strings are declared, read, copied, compared, searched and tokenized — is really just watching the same rule play out in different functions.

What Is a String in C?

A C string is nothing more than a run of characters sitting next to each other in memory, with a '\0' byte placed right after the last meaningful one. Nothing about the array's type marks it as a string — it's the presence of that trailing '\0', and the fact that string-handling code agrees to look for it, that makes the difference.

Even though "Hello" is five letters, the array actually reserves six slots — the five visible characters plus the terminator the compiler adds automatically. strlen(name) reports 5, because it counts only what comes before '\0', but the array itself is one element bigger than that count.

This gap between "how many characters strlen reports" and "how many bytes the array actually holds" comes up constantly through the rest of this page — it's the root cause behind most string-related bugs.

char name[] = "Hello";
// stored as: H  e  l  l  o  \0   (6 elements total)

Character vs String

A single char holds exactly one character and is written with single quotes, like 'A'. A string holds a sequence of characters plus a trailing '\0', and is written with double quotes, like "ABC". The quote style isn't cosmetic — 'A' and "A" are genuinely different things to the compiler: one is a lone char value, the other is a two-element array (the letter plus a terminator).

ch occupies a single byte. str occupies four bytes: 'A', 'B', 'C', and the terminator that makes it a valid string rather than a bare character array.

char ch = 'A';       // one character
char str[] = "ABC";  // three characters + '\0' terminator

Declaring a String

Declaring char name[20]; only reserves twenty bytes of space — it does not put a valid string inside them. Until something writes a '\0' somewhere in that array, there's no telling where a string function would consider the text to end, because whatever leftover bytes happen to be sitting in that memory are unpredictable.

name2 is safe to use immediately: the compiler writes 'R','i','n','a','\0' into the first five bytes and fills the remaining fifteen with zeros. name, left uninitialized, is not safe to pass to any string function until something gives it a proper terminator.

char name[20];             // reserved space, but not yet a valid string
char name2[20] = "Rina";   // properly initialized — a real string from the start

Three Ways to Initialize a String

There are three common ways to put a string into a char array, and it's worth being able to recognize all three when reading someone else's code.

All three produce the same five visible characters followed by a terminator. The first form is the most common because the compiler counts the characters for you; the third form is the most explicit about what's really happening — a plain array of chars that only counts as a "string" because the last element is '\0'.

In the character-by-character form, leaving out the trailing '\0' is entirely possible — the compiler won't add it for you the way it does for a string literal. Skip it, and you no longer have a valid C string, just a plain character array.

// 1. String literal — compiler sizes the array for you
char str1[] = "Hello";

// 2. Fixed size, filled from a literal — leftover bytes are zeroed
char str2[10] = "Hello";

// 3. Character by character — you place the '\0' yourself
char str3[] = {'H', 'e', 'l', 'l', 'o', '\0'};

The Null Character '\0'

'\0' is a byte whose numeric value is zero. It carries no printable glyph of its own — its entire job is to act as a stop sign for anything that walks through a string looking for the end.

Any function that processes str — strlen, printf's %s, strcpy, and the rest — starts at the first byte and keeps going until it reads a byte equal to zero. That's the one and only signal it uses to know the string is finished.

'\0' (the null character, value 0) is not the same thing as '0' (the printable digit character, value 48 in ASCII). Writing '0' where '\0' was intended leaves a stray digit in the string instead of terminating it — a subtle, easy-to-miss mistake.

char str[] = "CAT";
// laid out as: C  A  T  \0

String Length vs Array Size

strlen() and sizeof() answer two completely different questions about the same array, and mixing them up is one of the most common sources of confusion for anyone new to C strings.

strlen(str) walks the array and stops at the terminator, so it reports 5 — the current content length. sizeof(str), applied directly to the array (not to a pointer to it), reports the full 20 bytes the compiler set aside, regardless of how much of that space actually holds meaningful text right now.

This prints strlen: 5 and sizeof: 20 — one number describes what's stored right now, the other describes how much room exists in total.

char str[20] = "Hello";

// strlen counts characters BEFORE '\0'  -> 5
// sizeof reports the array's declared capacity -> 20 (assuming a 1-byte char)
#include <stdio.h>
#include <string.h>

int main() {
    char str[20] = "Hello";
    printf("strlen: %zu\n", strlen(str));
    printf("sizeof: %zu\n", sizeof(str));
    return 0;
}

String Literals

Anything written inside double quotes is a string literal, and the compiler automatically appends a '\0' after its last character. String literals can also contain escape sequences, which the compiler translates into single special bytes.

Each escape sequence like \n, \t, or \" is written as two characters in the source file but stored as a single byte in memory — the backslash is a signal to the compiler, not a character that ends up in the string itself.

"Hello"              // H e l l o \0
"Hello\n"             // adds a newline before the terminator
"Name:\tRina"         // adds a tab character
"She said \"Hello\""  // escaped quotes let a literal quote appear inside the string

Character Array vs Valid String

Not every array of char values qualifies as a C string. Whether it does depends entirely on whether a '\0' shows up somewhere inside it.

a is a perfectly ordinary three-element char array, but passing it to strlen, printf's %s, or any other <string.h> function is undefined behavior — those functions will keep reading past the array's end looking for a terminator that was never placed. b, with its explicit '\0', is safe to use anywhere a string is expected.

char a[3] = {'A', 'B', 'C'};       // NOT a valid string — no '\0' anywhere
char b[4] = {'A', 'B', 'C', '\0'};  // IS a valid string

Reading Strings with scanf

scanf's %s conversion reads a run of non-whitespace characters into a char array and appends '\0' after the last one it copies. Notice that the array's name is passed directly, with no leading & — an array expression already evaluates to the address of its first element, so adding & would actually be a type mistake here.

The 19 in %19s caps how many characters scanf is willing to write, leaving room for the '\0' it adds afterward inside a 20-byte array. Without that limit, an unbounded scanf("%s", name); will happily keep writing characters past the end of a 20-byte array if the typed input is longer, corrupting whatever memory sits next to it.

%s skips any leading whitespace, then starts copying characters.

Copying stops at the next whitespace character (space, tab, or newline) — that character is left unread in the input, not stored.

Given input like "Rina Sahu", one scanf("%s", name) call only captures "Rina" — "Sahu" is still waiting in the input buffer for a later read.

#include <stdio.h>

int main() {
    char name[20];
    scanf("%19s", name);   // no & — name already IS an address
    return 0;
}

Reading a Full Line with fgets

Because scanf's %s stops at the first whitespace, it can't capture a name or sentence containing spaces. fgets reads an entire line instead, up to a size limit you provide, which makes it the safer general-purpose choice for text input.

sizeof name (50 here) tells fgets the absolute most it may write, terminator included, so it can never overrun the array the way an unbounded scanf could. One quirk to watch for: if the newline the user typed still fits within that limit, fgets stores it as part of the string, right before the '\0' — code that cares about a clean string often has to strip it off afterward.

char name[50];
fgets(name, sizeof name, stdin);

Printing Strings

printf's %s conversion prints characters starting at the given address and keeps going until it reads a '\0'.

This is symmetric with how every other string function behaves: printf has no idea how big the underlying array is, only where to start and when to stop. Handing %s a pointer to data that never actually contains a '\0' is undefined behavior — printf will keep printing whatever bytes happen to follow in memory until it eventually stumbles on a zero byte somewhere.

printf("%s\n", name);

The <string.h> Header

<string.h> groups together the standard library's text-handling functions. Broadly, they split into two families: string-oriented functions that rely on finding '\0' to know where to stop, and memory-oriented functions that instead take an explicit byte count.

Every function on the first list expects the data it's given to actually be null-terminated. Every function on the second list works on any block of bytes at all, terminated or not, as long as you tell it exactly how many bytes to touch.

String-oriented (terminator-based):
  strlen, strcpy, strncpy, strcat, strncat,
  strcmp, strncmp, strchr, strrchr, strstr, strtok

Memory-oriented (byte-count-based):
  memcpy, memmove, memset, memcmp, memchr

strlen — Finding a String's Length

strlen(str) walks forward from the first character and counts how many it passes before hitting '\0' — the terminator itself is never included in the count.

Because strlen has to scan the whole string character by character to find the terminator, it's an O(n) operation, not a constant-time lookup — there's no stored length field it can just read.

A loop written as for (i = 0; i < strlen(str); i++) recomputes strlen(str) — a full rescan of the string — on every single iteration. Computing the length once into a variable before the loop starts, and checking against that variable instead, avoids redoing that scan over and over.

size_t length = strlen(str);

strcpy — Copying a String

strcpy(destination, source) copies characters from source into destination one by one, including the terminator, and stops the instant it writes that final '\0'.

strcpy never checks whether destination actually has enough room. Copying "Hello" — five characters plus a terminator, six bytes in total — into a destination array declared with only five elements overruns it by one byte, and strcpy has no way of noticing that on its own; ensuring enough space is entirely the caller's job.

strcpy(destination, source);

strncpy — A Size-Limited Copy

strncpy(destination, source, n) copies at most n characters from source into destination. It sounds like the safe version of strcpy, but it has a sharp edge worth knowing about.

If src is n characters long or longer, strncpy fills all n bytes of dest with copied characters and stops — it does NOT add a '\0' of its own in that case, leaving dest unterminated. The pattern shown here — copy n-1 characters, then explicitly write the terminator at the last slot — is the standard way to use strncpy safely regardless of how long src turns out to be.

strncpy(dest, src, sizeof dest - 1);
dest[sizeof dest - 1] = '\0';   // guarantee termination yourself

strcat — Joining Two Strings

strcat(first, second) finds the '\0' at the end of first, then starts copying second's characters right there, overwriting that terminator and adding a new one after everything second contributed.

first has to have enough spare capacity beyond its current content to hold all of second's characters plus the new terminator — strcat, like strcpy, does no bounds checking of its own.

strcat(first, second);

strncat — A Size-Limited Append

strncat(destination, source, n) appends at most n characters from source onto the end of destination, and — unlike strncpy — it always writes a terminator afterward.

destination still needs enough total room for its existing content, plus up to n appended characters, plus the terminator strncat adds at the end — that arithmetic is still the caller's responsibility to get right.

strncat(destination, source, n);

Comparing Strings the Wrong Way

It's tempting to compare two strings with ==, the same way you'd compare two integers, but that comparison doesn't do what it looks like it does.

str1 and str2 are pointers (or decay to pointers here), so == checks whether they point at the exact same memory location — not whether the characters stored there match. Two separate arrays holding the identical text "Hello" will still compare unequal with ==, because they live at two different addresses.

Comparing string contents correctly means calling strcmp() and checking its result, never comparing the pointers themselves with ==.

if (str1 == str2) { /* ... */ }   // compares ADDRESSES, not contents

strcmp — Comparing String Contents

strcmp(str1, str2) walks both strings together and compares them character by character. It returns 0 when the strings are identical, a negative value when str1 sorts before str2, and a positive value when str1 sorts after str2.

Only the sign of a nonzero result is guaranteed by the standard — a specific value like -1 or 1 is not part of the contract, so code should only ever ask whether the result is zero, negative, or positive, never compare it against an exact number.

if (strcmp(name1, name2) == 0) {
    printf("Names match\n");
}

strncmp — Comparing a Prefix

strncmp(str1, str2, n) works exactly like strcmp, except it only ever looks at the first n characters of each string, ignoring anything after that.

This is useful for checking a common prefix — matching a command name, a file extension, or the start of an identifier — without caring what follows it.

if (strncmp(str1, str2, 3) == 0) {
    printf("First 3 characters match\n");
}

strchr — Finding the First Occurrence of a Character

strchr(str, ch) scans str from the beginning and returns a pointer to the first place ch appears, or NULL if it never shows up.

The returned pointer lands right at the matching character, which means everything from that point onward — including the rest of the original string — can be read directly through it.

char *pos = strchr("hello world", 'o');
// pos points at the 'o' in "hello", the FIRST match

strrchr — Finding the Last Occurrence of a Character

strrchr behaves like strchr, but it searches from the end of the string backward instead of from the beginning forward, so it finds the LAST matching character rather than the first.

This distinction matters for something like pulling a file extension out of a path — the last '.' is almost always the one that separates the name from the extension, not the first.

char *pos = strrchr("hello world", 'o');
// pos points at the 'o' in "world", the LAST match

strstr — Finding a Substring

strstr(haystack, needle) looks for the first place the entire needle string appears inside haystack, and returns a pointer to where the match starts, or NULL if needle doesn't appear at all.

Unlike strchr, which only ever looks for a single character, strstr matches a whole sequence of characters in order — a genuine substring search rather than a character search.

char *match = strstr("Learning C programming", "C prog");
// match points at "C programming"

Tokenizing with strtok

strtok splits a string into pieces (tokens) wherever one of a given set of delimiter characters appears. The first call passes the string itself; every call after that passes NULL, and strtok remembers internally where it left off.

Each call returns the next token and internally overwrites the delimiter that followed it with a '\0', which is how strtok is able to hand back a normal, independently null-terminated string for each piece.

strtok modifies the string it's given, replacing delimiters with '\0' characters in place. Because of that, it must never be handed a string literal like strtok("a,b,c", ",") — string literals aren't writable, and modifying one is undefined behavior. Pass it a writable char array instead.

char protocols[] = "CAN,SPI,I2C";
char *token = strtok(protocols, ",");
while (token != NULL) {
    printf("%s\n", token);
    token = strtok(NULL, ",");
}
// prints: CAN  then  SPI  then  I2C

String Functions vs Memory Functions

It's worth drawing a clear line between the two families of functions <string.h> provides. strlen, strcpy, strcat, strstr and the like are string-oriented — they expect null-terminated input and rely on finding '\0' to know when to stop. memcpy, memmove, memset and memcmp are memory-oriented — they work on a raw block of bytes and take an explicit count, with no interest in whether a '\0' shows up anywhere in that block.

This distinction matters in practice: memory-oriented functions can safely operate on data that isn't a string at all — an array of ints, a struct, image pixel data — because they never go looking for a terminator that might not even be meaningful there.

memcpy — Copying an Exact Number of Bytes

memcpy(destination, source, number_of_bytes) copies exactly the byte count you specify, with no attention paid to '\0' at all.

Because memcpy never searches for a terminator, it's just as happy copying raw binary data — a block of ints, a struct, part of an image buffer — as it is copying text.

memcpy(destination, source, number_of_bytes);

memmove — Safe for Overlapping Regions

memmove behaves like memcpy, but it's specifically designed to work correctly even when the source and destination regions overlap.

Calling memcpy with overlapping source and destination regions is undefined behavior — the copy might silently corrupt data that hasn't been read yet. Whenever the two regions might overlap, memmove is the function to reach for instead.

memset — Filling a Block of Memory

memset(pointer, value, number_of_bytes) writes the same byte value into every one of the given number of bytes starting at pointer.

Zeroing a char buffer this way effectively fills it with '\0' bytes throughout, which is a common way to reset a string buffer to a clean, empty state before reusing it.

memset(buffer, 0, sizeof buffer);   // zero out every byte

memcmp — Comparing an Exact Number of Bytes

memcmp(a, b, n) compares exactly n bytes of a against n bytes of b, with the same 0 / negative / positive result convention as strcmp.

Because memcmp doesn't stop at '\0', it can compare data that isn't null-terminated at all — two raw byte buffers, two structs, or arrays that intentionally contain zero bytes as legitimate data rather than as a terminator.

String Copy vs Memory Copy at a Glance

It helps to see the string-copying and memory-copying functions side by side, since they solve overlapping but distinct problems.

The rule of thumb: reach for the str* functions when working with actual text, and the mem* functions when working with a fixed-size block of raw data, especially one that might not be null-terminated at all.

Function    Operates on         Needs '\0'?   Notes
--------    -----------------    -----------   -----------------------------
strcpy      null-terminated str  yes            copies until it writes '\0'
strncpy     null-terminated str  yes (usually)  may NOT terminate dest itself
memcpy      raw byte block       no             copies exactly n bytes
memmove     raw byte block       no             safe when regions overlap

How a String Actually Sits in Memory

There's nothing abstract about any of this — a string is genuinely just consecutive bytes in memory, one after another, ending at a byte that happens to be zero.

char name[] = "Rina"; laid out as five consecutive bytes. The terminator at name[4] is what tells every string function where the meaningful data stops.

Modifying a String in Place

A character array built from a string literal is ordinary writable memory — nothing stops you from changing individual characters after the fact, as long as the write stays inside the array's bounds.

name is a local array that was initialized by copying the literal's characters into it — changing name[0] only changes that copy, and does nothing to the original literal text.

char name[] = "Hello";
name[0] = 'Y';
printf("%s\n", name);   // prints "Yello"

String Literal vs Character Array

There's a sharp difference between char array[] = "text"; and char *p = "text";, even though they look similar at a glance.

a owns its own storage, so writing to a[0] is completely fine. p instead points directly at the literal "Hello" stored by the compiler, which is typically placed in read-only memory — writing through p is undefined behavior. Whenever a writable copy is actually needed, char p[] = "Hello"; (an array, not a pointer) is the correct choice.

char a[] = "Hello";   // a writable ARRAY, holding a copy of the characters
char *p  = "Hello";   // a pointer AT the literal itself — do not modify it

p[0] = 'Y';   // undefined behavior — string literals must not be modified

Capacity vs Current Content

It's useful to keep capacity (how many bytes an array was declared with) and current content (how much of that space is actually holding meaningful text right now) as two separate mental categories.

There's room here for the string to grow later — via strcat, for instance — as long as whatever gets written never exceeds those 20 reserved bytes.

char name[20] = "Rina";
// capacity: 20 elements available
// current content: 4 characters + '\0' = 5 bytes actually in use

Common Mistake: A Missing Terminator

Filling every element of a char array by hand, without leaving room for or explicitly adding a '\0', produces something that looks like a string but isn't one.

All five bytes are used up by the letters themselves, so there's nowhere for a terminator to go — printf's %s will read straight past the end of str looking for one, into whatever memory happens to follow.

The fix is simple: either size the array for six elements and add '\0' as the sixth, or just switch to a string literal initializer like char str[] = "Hello"; and let the compiler handle it correctly.

char str[5] = {'H', 'e', 'l', 'l', 'o'};  // no room left for '\0'
printf("%s", str);                        // undefined behavior

Common Mistake: A Destination That's Too Small

It's easy to declare a destination array sized for the visible characters only, and forget the extra byte the terminator needs.

"Hello" needs 5 characters plus a terminator — 6 bytes total — but dest was only given 5, so strcpy writes one byte past the end of the array.

The fix: size dest for at least 6 elements (N visible characters always need at least N+1 elements of storage) — for example char dest[6]; or a larger, more generous size.

char dest[5];
strcpy(dest, "Hello");   // needs 6 bytes, dest only has 5

Common Mistake: Comparing Strings with ==

As covered earlier, comparing two char* or char[] values with == checks addresses, not the text they contain — a mistake worth calling out again here alongside the other common pitfalls.

strcmp actually reads through both strings and compares their characters; == only ever compares where the two pointers happen to point.

if (strcmp(name1, name2) == 0) {
    // correct way to check whether the CONTENTS match
}

Common Mistake: Treating strlen as Capacity

strlen(buffer) and sizeof(buffer) are easy to conflate, but they answer different questions — one gives current length, the other gives total capacity — and treating strlen's result as if it were the buffer's size is a recurring source of bugs.

strlen(buffer) tells you how much of the buffer currently holds text. sizeof(buffer), applied directly to the array itself, tells you the buffer's total capacity. Using strlen's result where a capacity check was actually needed silently ignores however much unused space the buffer might still have — or, worse, assumes room that doesn't actually exist.

Common Mistake: sizeof After a Function Parameter Decays

Inside a function that receives an array parameter, sizeof no longer reports the original array's size — it reports the size of a pointer instead, because array parameters are silently adjusted to pointers.

Even though str is written with array syntax in the parameter list, the compiler treats it as char *str — the array-size information from the caller's side simply isn't available inside the function. If the function actually needs to know the buffer's capacity, that number has to be passed in separately as an extra argument.

void print_string(char str[]) {
    // sizeof(str) here is the size of a POINTER, not the original array
    printf("%s\n", str);
}

Arrays of Strings

A two-dimensional char array can hold several fixed-size strings at once — one string per row, each row a separately null-terminated string of its own.

Each of the three rows has room for up to 19 visible characters plus its own '\0' — names[0] holds "Rina", names[1] holds "Amit", and so on. This layout is handy whenever the number of strings and a reasonable maximum length are both known ahead of time.

char names[3][20] = {"Rina", "Amit", "Rahul"};

Worked Example: Reading and Displaying a Name

Putting several of the ideas above together, here's a small complete program that reads a full name (spaces included), strips the trailing newline fgets may have left behind, and reports its length.

strcspn(name, "\n") reports how many characters lead up to the first '\n' it finds — that's exactly where the newline sits, so overwriting that position with '\0' cuts it off cleanly, leaving name holding just the typed text.

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

int main() {
    char name[50];
    printf("Enter your name: ");
    fgets(name, sizeof name, stdin);

    name[strcspn(name, "\n")] = '\0';   // strip a trailing newline, if present

    printf("Hello, %s!\n", name);
    printf("Length: %zu\n", strlen(name));
    return 0;
}

Worked Example: Copying and Comparing Strings

This example copies a string with strcpy, then uses strcmp to confirm the copy really did produce identical content.

copy is declared with the same 20-byte capacity as source, so the strcpy call is safe. strcmp then confirms, character for character, that copy really does hold the same text as source.

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

int main() {
    char source[20] = "Embedded";
    char copy[20];

    strcpy(copy, source);

    if (strcmp(source, copy) == 0) {
        printf("Strings are equal\n");
    }
    return 0;
}

Worked Example: Concatenating Two Strings

This example joins two separate strings into one with strcat, taking care to size the destination array large enough for the combined result up front.

result is declared with 30 bytes of capacity, comfortably more than "Embedded " and "Systems" combined need — strcat appends "Systems" right after the existing text, replacing the old terminator and adding a new one at the very end.

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

int main() {
    char result[30] = "Embedded ";
    strcat(result, "Systems");
    printf("%s\n", result);   // "Embedded Systems"
    return 0;
}

Quick Reference: <string.h> Functions

A single scannable table of the functions covered on this page, for whenever a quick reminder of what each one does is all that's needed.

strcspn and strspn are near-mirror images of each other: strcspn measures how far you get before hitting any character from a given set, while strspn measures how far you get while every character belongs to that set.

Function   Purpose
---------  --------------------------------------------------
strlen     length of a string, not counting the terminator
strcpy     copy a string, including its terminator
strncpy    copy up to n characters (may not null-terminate)
strcat     append one string onto the end of another
strncat    append up to n characters, always null-terminates
strcmp     compare two strings' contents
strncmp    compare the first n characters of two strings
strchr     find the first occurrence of a character
strrchr    find the last occurrence of a character
strstr     find the first occurrence of a substring
strtok     split a string into tokens by delimiter
strcspn    length of the initial segment with NONE of a char set
strspn     length of the initial segment made ONLY of a char set
memcpy     copy an exact number of bytes
memmove    copy bytes, safe even if regions overlap
memset     fill a block of memory with a byte value
memcmp     compare an exact number of raw bytes
memchr     find a byte value within a block of raw memory

The Safety Rule Behind Every Buffer Overflow

Almost every classic function in <string.h> — strcpy chief among them — has no idea how big its destination buffer actually is. It trusts the caller completely to have already made sure there's enough room.

This one gap — functions that write without checking, paired with programmers who miscalculate how much space is really needed — is the single biggest recurring source of buffer overflows in C, and one of the main reasons the language has such a long history of memory-safety bugs.

Practical Rules for Writing Safe String Code

A short checklist worth keeping in mind whenever strings are involved.

None of these rules are exotic — they're just the direct, practical consequence of everything covered above: strings are plain arrays, functions trust the caller, and only '\0' marks where the real content ends.

- N visible characters need AT LEAST N+1 elements of storage (room for '\0')
- Prefer bounded input: scanf("%19s", ...) or fgets(buf, sizeof buf, stdin)
- When sizing a buffer, always account for the terminator
- Never write through a char* that points at a string literal
- Never compare string contents with == — always use strcmp

Mental Model: Following a String End to End

It helps to hold the whole chain of ideas in mind as one sequence: a C string is an array of characters; those characters sit in consecutive memory; the array ends, as far as string functions are concerned, at a '\0' byte; and every function in <string.h> relies on finding that byte to know where its job is done.

"ENGINEER" is 8 letters, so the array the compiler builds is 9 bytes — the 8 letters plus the '\0' that turns them into a real, usable C string.

char str[] = "ENGINEER";
// 8 visible characters + 1 terminator = 9 elements of storage

String Operations vs Memory Operations, Revisited

Bringing the whole page full circle: string operations (strlen, strcpy, strcat, strcmp, strstr, and the rest) all understand and depend on '\0' to know where their work ends. Memory operations (memcpy, memmove, memset, memcmp) instead work against an exact byte count you supply, and never care whether a '\0' appears anywhere in that range.

Reaching for the right family for the job — string functions for actual text, memory functions for raw or possibly non-terminated data — is one of the most reliable ways to avoid the subtle bugs this page has walked through.

A C string is a char array whose last meaningful character is followed by a null terminator, the character '\0' (value 0), which marks where the string ends. String literals like "hello" automatically get this null terminator appended by the compiler.

Without a dedicated string type, C needs some way to know where a sequence of characters stops — since arrays don't inherently track their own length, the null terminator convention is what every string-handling function in the standard library agrees to rely on.

Names, messages, file paths, command-line arguments — any text at all in a C program is represented this way, and understanding it is essential before ever touching file I/O or user input seriously.

A C string is like a sentence written on a strip of paper with no length label on it — the only way to know where it ends is a special 'stop' stamp placed right after the last letter. Every function that reads the strip knows to keep reading until it sees that stamp, never before.

strlen and strcat on a char array Declares a char array holding "Alice", measures its length, appends an exclamation mark, and prints the result.

char name[20] = "Alice"; reserves 20 bytes and stores A, l, i, c, e, followed by a \0 automatically — 6 bytes used, 14 left unused but still reserved.

strlen(name) walks forward from name[0] counting characters until it hits the \0, returning 5 — note it does NOT count the terminator itself.

strcat(name, "!") finds name's existing \0, overwrites it with '!', then writes a NEW \0 right after — this only works safely because name's buffer (20 bytes) had room left for the extra character.

The final printf uses %s, which tells printf to print characters starting at name's address and keep going until it finds the \0 — printing "Alice!" and stopping there, ignoring the rest of the unused buffer.

'A''l''i''c''e''\0' … Only 6 of the 20 reserved bytes are meaningful — everything after the '\0' is unused buffer space that string functions never look at.

Comparing strings correctly Shows why == is the wrong tool for comparing string content, and strcmp is the right one.

a and b are two separate char arrays that happen to hold the same text — they live at two different addresses in memory.

a == b compares those two addresses, not the characters inside them — since the addresses differ, this prints 'Pointers different', even though the text is identical.

strcmp(a, b) instead walks both strings character by character and returns 0 only when every character (up to and including the terminator) matches — here it correctly reports 'Contents equal'.

char buf[5]; strcpy(buf, "hello"); // "hello" needs 6 bytes char buf[6]; strcpy(buf, "hello");

"hello" has 5 visible characters plus a required '\0' terminator — 6 bytes total. A 5-byte buffer is one byte too small, and strcpy has no way to know that and will write past the end anyway.

if (str1 == str2) { /* compare text */ } if (strcmp(str1, str2) == 0) { /* compare text */ }

== on two char* values compares the pointers (addresses), not the characters they point to. Two different buffers holding identical text are never == equal — strcmp is what actually compares content.

Key <string.h> functions: strlen (length, excluding the terminator), strcpy/strncpy (copy), strcat/strncat (append), strcmp (lexicographic compare, returns 0 if equal), and strchr/strstr (search for a character/substring).

A string literal in your source code, like "hello", is stored in read-only memory by most compilers — attempting to modify it through a char* pointing at it (rather than a char array you own) is undefined behavior, often crashing immediately.

The buffer holding a string must always be at least strlen(text) + 1 bytes — the +1 is for the null terminator, a detail that's extremely easy to forget when sizing a buffer.

strcpy and strcat have no idea how big the destination buffer actually is — they'll happily write past its end if the source is too long, which is exactly why the safer strncpy/strncat (or bounds-checked alternatives) exist.

Forgetting the null terminator when building a char array manually (rather than via a string literal), leaving strlen and every other string function to read garbage memory indefinitely until they happen to hit a zero byte somewhere.

Sizing a buffer for exactly strlen(text) bytes instead of strlen(text) + 1, silently clipping off room for the terminator and corrupting whatever memory comes right after the buffer.

Using strcpy/strcat with untrusted or unpredictable input length without checking the destination buffer's actual capacity first — the single most common real-world C security vulnerability (buffer overflow), covered in depth in the Undefined Behavior & Common Pitfalls topic.

Comparing two strings with == instead of strcmp — == compares the two pointers' addresses, not the text they point to, so it's almost never what you actually want for strings.

Preferring strncpy/snprintf over strcpy/sprintf in any code that touches untrusted input, explicitly bounding how many bytes can be written.

Always allocating strlen(text) + 1 bytes (or more) for any buffer meant to hold a copy of text, as an automatic habit.

Using strcmp(a, b) == 0 as the idiomatic way to check string equality in C, since there's no built-in == for string content comparison.

String literal pooling and read-only storage Many compilers store identical string literals only once in memory and mark that memory read-only — two separate "hello" literals in your code might even share the exact same address. This is precisely why modifying a string literal through a char* (char *s = "hi"; s[0] = 'H';) is undefined behavior and commonly segfaults, while modifying a char array initialized from a literal (char s[] = "hi"; s[0] = 'H';) is perfectly safe, since that creates the programmer's own separate, writable copy.

Why C's string design trades safety for simplicity A null-terminated string needs no extra length field stored alongside it, keeping the representation minimal — but it also means computing a string's length is always an O(n) walk (unlike a length-prefixed string design, used internally by many higher-level languages), and it means a stray \0 written in the wrong place silently truncates a string with no warning at all.

What character marks the end of a C string, and what's its value?

The null terminator, '\0', which has the integer value 0. Every standard string function relies on scanning for this byte to know where a string ends.

Why is if (str1 == str2) almost always wrong for comparing two C strings?

== compares the two pointers' memory addresses, not the actual characters they point to. Two different buffers holding identical text would compare as unequal with ==; strcmp(str1, str2) == 0 is the correct way to check content equality.

Why must a buffer meant to hold a copy of a string be at least strlen(text) + 1 bytes?

The +1 accounts for the null terminator, which is not counted by strlen but must still physically be written into the buffer for the string to be valid and safely readable by other string functions.

Why does the array behind char msg[] = "Hi"; need 3 bytes instead of 2?

The string literal "Hi" contributes its two visible characters, but the compiler also appends a '\0' terminator automatically, so the array actually needs three bytes: 'H', 'i', and '\0'.

What's the difference between what strlen() and sizeof() report for the same char array?

strlen() counts only the characters before the '\0' terminator — the string's current content length. sizeof(), applied to the array itself, reports the array's total declared capacity in bytes, regardless of how much of it is actually in use.

Why is comparing two strings with == usually a mistake?

== on two char* or array values compares their addresses, not their contents, so two different arrays holding identical text will still compare as unequal. strcmp() should be used instead to compare the actual characters.

Why must strtok() never be called on a string literal?

strtok() modifies the string it's given in place, overwriting delimiter characters with '\0' as it works. String literals aren't writable, so passing one to strtok() results in undefined behavior.

What's the practical difference between memcpy() and the string functions like strcpy()?

strcpy() copies characters until it writes a '\0' and assumes the source is null-terminated. memcpy() instead copies an exact number of bytes you specify and never looks for a terminator, so it works on any block of data, text or otherwise.

Write a C program to find the length of a string without using the strlen() function.

The function walks the array one character at a time, incrementing a counter until it reaches the '\0' terminator, then returns that count — exactly what strlen does internally.

Write a C program to copy one string into another without using the strcpy() function.

Each character is copied from src to dest in a loop until the source's terminator is reached, and the terminator is then written into dest by hand to make the copy a valid string.

Write a C program to reverse a string in place without using the strrev() function.

Two indices start at opposite ends of the string and swap the characters they point to, moving toward the middle, which reverses the string in place without needing a second buffer.

Write a C program to check whether a given string is a palindrome.

Comparing characters from both ends moving inward confirms a palindrome if every pair matches all the way to the middle; a single mismatch is enough to rule it out.

Write a C program to count the number of vowels and consonants in a string.

Each character is lowercased for a case-insensitive check, then tested against the vowel set; any other alphabetic character is counted as a consonant, and non-letters like spaces are simply skipped.

Write a C program to count the number of words in a sentence.

The scan tracks whether it is currently inside a word; a new word is counted each time a non-space character follows a space (or the start of the string), which correctly counts words separated by single spaces.

Write a C program to convert a string to uppercase without using library functions like toupper().

Since the lowercase and uppercase letters are laid out in the same relative order in ASCII, subtracting 'a' and adding 'A' shifts any lowercase letter to its uppercase equivalent while leaving other characters untouched.

Write a C program to remove all spaces from a string.

A second index tracks where the next kept character should go; every non-space character is copied into place, and the string is re-terminated once the scan finishes, effectively closing up the gaps left by removed spaces.

Write a C program to check whether two strings are anagrams of each other.

A count array tallies how many times each byte value appears in the first string and un-tallies it for the second; if every count lands back at zero, both strings used exactly the same characters the same number of times.

Write a C program to concatenate two strings without using the strcat() function.

The function first walks dest to find its existing terminator, then copies src's characters starting right at that position, finishing with a fresh '\0' — the same behavior strcat provides, written out by hand.

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

int main() {
    char name[20] = "Alice";
    printf("Length: %zu\n", strlen(name));

    strcat(name, "!");
    printf("Greeting: Hi, %s\n", name);
    return 0;
}
#include <stdio.h>
#include <string.h>

int main() {
    char a[] = "cat";
    char b[] = "cat";

    if (a == b) printf("Pointers equal\n");
    else printf("Pointers different\n");

    if (strcmp(a, b) == 0) printf("Contents equal\n");
    return 0;
}