Topic 68 of 100 Arrays and Strings in C Interactive Tutorial

Strings in C: Character Arrays, Declaration, Initialization, and Usage

13 mins read Last updated: 22 Aug 2026 29,800 views
C Compiler 0.00%

What is Strings in C? Core Concept & Direct Summary

Core Objective & Problem Statement

What is a string in C, how does null-termination (`\0`) define string boundaries, and what is the difference between char arrays and string literals?

Direct Answer Summary

Here's a fact that surprises almost everyone learning C: C doesn't have a string type. Not a keyword, not a built-in type, not a class. Nothing like Python's str, Java's String, or C++'s std::string.

What C has instead is a convention. A string is a char array that ends with a special character — the null terminator, '\0'. That's it. That's the whole definition. And once you internalize it, every C string behavior that seems strange suddenly makes sense: why strlen exists, why buffers need one extra slot, why forgetting the terminator crashes things, why strings can be modified in some places and not others.

This is the foundation page for our Strings cluster. Let's build the model properly from the ground up.

(Suggested image: the hero graphic — a char array box labeled "Hello" showing five characters plus a distinctly-colored '\0' box at the end, with the caption "a string IS a char array + '\0'")

How Does Strings in C Work? In-Depth Mechanics & Architecture

The Definition: A Char Array Ending in '\0'

Memory:

main.c
Run Online
Index: 0 1 2 3 4 5Char: 'H' 'e' 'l' 'l' 'o' '\0'

Six slots for five visible characters. The '\0' (null character — value 0, not the same as the character '0' which is value 48) is what marks the end.

Without it, it's not a string. It's just a character array:

main.c
Run Online
char letters[] = {
    'H', 'e', 'l', 'l', 'o'
}
;
// char array — NO terminatorchar greeting[] = "Hello";                      // STRING — C adds '\0' for you

These two look similar, but only the second is a proper string. Pass the first to strlen, printf("%s"), or any string.h function, and it will keep reading past the end of the array into whatever memory follows — the same runaway-buffer problem we met with gets().

That's why the double-quoted form is preferred."Hello" automatically gets its terminator; manual char-by-char initialization doesn't.

(Suggested image: side-by-side comparison of {'H','e','l','l','o'} (no terminator, marked dangerous) vs "Hello" (with terminator, marked safe) — this is the single most important visual on the page)


Every Way to Declare and Initialize a String

Here's the complete set, with what each produces.

Form 1: Array with explicit size, initialized from a literal

20 bytes total, contents 'R','a','v','i','\0', then 15 unused bytes. sizeof(name) = 20, strlen(name) = 4. The buffer has capacity for a 19-character string — the remaining bytes let you grow the text later (up to 19 characters).

Form 2: Size inferred from the literal

Exactly 5 bytes: the 4 characters plus the terminator. sizeof = 5, strlen = 4. This is fixed — you cannot append anything, because there's no spare room. Attempting strcat here overflows.

The design question between forms 1 and 2: do you need room to grow? Form 1 reserves it; Form 2 doesn't. For input, always use a sized buffer.

Form 3: Character-by-character with an explicit terminator

main.c
Run Online
char name[5] = {
    'R', 'a', 'v', 'i', '\0'
}
;

Equivalent to Form 2, but you must remember '\0' yourself. Valid but unnecessary in modern code.

Form 4: Zero-fill then fill later

main.c
Run Online
char buffer[100] = {
    0
}
;
// all 100 bytes are '\0'

Extremely useful for input buffers — you start with a guaranteed-valid empty string plus a generous margin.

Form 5: Empty string (valid, zero length)

One '\0' and nine unused bytes. strlen(empty) = 0. Legal and frequently used as a starting point for building strings.

Form 6: A pointer to a string literal

⚠️ This is a different beast entirely — read on.


String Literals vs Char Arrays: The Critical Difference

main.c
Run Online
char arr[] = "Hello";
// an ARRAY — your own writable copychar *ptr   = "Hello";
// a POINTER to a READ-ONLY literal

What's actually happening:

  • char arr[] = "Hello"; copies the characters into a fresh array you own. You can modify it freely.
  • char *ptr = "Hello"; stores the address of a string literal that lives in read-only memory. ptr is just a pointer variable.
main.c
Run Online
char arr[] = "Hello";
arr[0] = 'J';
// ✓ perfectly fine — "Jello"printf("%s\n", arr);
char *ptr = "Hello";
ptr[0] = 'J';
// ✗ UNDEFINED BEHAVIOR — likely a crash!

On most modern systems, that second line causes a segmentation fault, because string literals are placed in a read-only segment. It compiles without complaint — it's a runtime surprise.

This also explains a famous crash with strtok: remember from where we split strings that strtokmodifies its input? Passing a literal crashes:

main.c
Run Online
strtok("a,b,c", ",");
// ✗ crash — trying to write to read-only memorychar data[] = "a,b,c";
strtok(data, ",");
// ✓ writable copy

Rule worth framing on your wall:char arr[] = "text" for strings you'll modify; char *ptr = "text" only for strings you'll read. When in doubt, use the array form.

(Suggested image: two memory regions diagram — read-only literal pool (with a padlock icon) vs stack array (with a pencil icon), showing where the pointer and the array each live)


Accessing and Modifying Characters

Strings are arrays, so indexing works exactly as it did in the arrays cluster:

main.c
Run Online
#include <stdio.h>
int main(void) {
    char word[] = "Hello";
    printf("%c\n", word[0]);
    // H    printf("%c\n", word[4]);        // o    printf("%c\n", word[5]);        // '\0' — the terminator! prints as nothing
    word[1] = 'a';
    // modify a character    printf("%s\n", word);           // Hallo
    return 0;
}

Traversing a string — two equally good patterns:

main.c
Run Online
char text[] = "Code";
// Pattern A: index-based, using the terminator as the stopping conditionfor (int i = 0; text[i] != '\0'; i++) {    putchar(text[i]);}
// Pattern B: pointer-basedfor (char *p = text; *p != '\0'; p++) {    putchar(*p);}

Both print Code. Pattern A uses the array indexing you already know; Pattern B is the pointer view (its own cluster, but you'll see it everywhere in C code).

Notice that you can't get the length with sizeof reliably once the string is passed to a function (the decay problem from the arrays cluster), but text[i] != '\0' as a loop condition always works — the terminator is in the data, not just in the declaration. That's the entire reason the '\0' convention exists: it makes strings self-terminating at runtime.

(Suggested image: both loop patterns shown side by side with the memory strip beneath, arrows showing each step)


Reading Strings From the User

This is where beginner pain concentrates, so let's be thorough.

scanf("%s", ...) — words only

main.c
Run Online
char name[30];
printf("Enter your name: ");
scanf("%s", name);
// note: no & needed — array name is an addressprintf("Hello, %s\n", name);

Two problems:

  1. It stops at the first whitespace. Type "Priya Sharma" and you get "Priya" — the rest stays in the input buffer, potentially breaking your next read.
  2. No bounds checking by default. Type 200 characters into a 30-byte buffer and you overflow it. Always add a width limit one less than the buffer size:
main.c
Run Online
scanf("%29s", name);
// ✓ reads at most 29 chars + '\0' = 30

When it's fine: reading single words, IDs, or short tokens where spaces genuinely aren't expected.

fgets() — full lines, safely

main.c
Run Online
char line[100];
printf("Enter a sentence: ");
fgets(line, sizeof(line), stdin);
// reads a whole line, respects the limitprintf("You typed: %s", line);

fgets reads until a newline (or until the buffer is full, whichever comes first), and it never overflows — the size you pass is the guarantee. This is why it's the recommended way to read text in C.

Its one quirk: it keeps the trailing newline. Strip it with the idiom you've seen throughout this series:

main.c
Run Online
#include <string.h>
line[strcspn(line, "\n")] = '\0';
// replace the newline with a terminator

(Fix that a nifty strcspn trick does in one line — and why we don't just write line[strlen(line)-1] = '\0'; is worth noting: that crashes on an empty string.)

getchar() in a loop — maximum control

From the character I/O tutorials — reading characters one at a time is the fully manual option, useful when you need custom stopping rules.

And the one to NEVER use

main.c
Run Online
char name[30];
gets(name);
// ✗✗ REMOVED from C11 — dangerous, unbounded

gets has no bounds checking whatsoever and was removed from the language. fgets is its replacement. If you see gets in old code, convert it.

Quick decision guide:

SituationUse
Single word, no spacesscanf("%29s", buf) (with width limit)
Full line of textfgets(buf, sizeof(buf), stdin)
Character-by-character controlgetchar() loop
Anything elseNever gets

Printing Strings

main.c
Run Online
char name[] = "Ravi";
printf("%s\n", name);
// Ravi — no & neededputs(name);
// Ravi + automatic newlineprintf("%.2s\n", name);
// Ra — precision limits the characters printed

Three notes: printf("%s") needs no & (the array name is already an address); puts adds a newline automatically; and the %.2s precision form (from the format specifiers tutorial) is a handy way to truncate output to a fixed width.

And the reverse — printing a string character by character:

main.c
Run Online
for (int i = 0;
name[i] != '\0';
i++) {
    putchar(name[i]);
}

The loop-based version is what you'd use when you need to inspect or transform each character as you print.


sizeof vs strlen for Strings (One More Time)

This distinction deserves repeating because it causes so much confusion:

main.c
Run Online
char message[50] = "Hello";
sizeof(message) // 50 — the BUFFER's capacity in bytesstrlen(message)      // 5  — the TEXT's length, excluding '\0'
sizeofstrlen
MeasuresThe containerThe contents
Includes '\0'?YesNo
When knownCompile timeRuntime (walks the string)
Works after passing to a function?No (decays to pointer)Yes (terminator-based)

Practical uses: sizeof when reserving buffer space (for fgets), strlen when you want to know how much text is actually there. And notice strlen is the only one that keeps working after the array decays into a pointer — because it relies on data (the terminator), not on declaration metadata.


A Complete Example: A Simple Login System

main.c
Run Online
#include <stdio.h>
#include <string.h>
#define USERNAME_LEN 30
#define PASSWORD_LEN 30
int main(void) {
    // Stored credentials (in real code these would never be hard-coded!)    char storedUser[] = "admin";    char storedPass[] = "secr3t2026";
    char inputUser[USERNAME_LEN];
    char inputPass[PASSWORD_LEN];
    printf("Username: ");
    fgets(inputUser, sizeof(inputUser), stdin);
    inputUser[strcspn(inputUser, "\n")] = '\0';
    // strip newline
    printf("Password: ");
    fgets(inputPass, sizeof(inputPass), stdin);
    inputPass[strcspn(inputPass, "\n")] = '\0';
    // Compare with strcmp — remember, == compares addresses!    if (strcmp(inputUser, storedUser) == 0 &&        strcmp(inputPass, storedPass) == 0) {        puts("\n✓ Login successful!");        printf("Welcome back, %s.\n", inputUser);    } else {        puts("\n✗ Login failed.");        if (strcmp(inputUser, storedUser) != 0) {            puts("  Username not recognized.");        } else {            puts("  Incorrect password.");        }    }
    return 0;
}

Sample Run (success):

main.c
Run Online
Username: admin
Password: secr3t2026
✓ Login successful!
Welcome back, admin.

Sample Run (wrong password):

main.c
Run Online
Username: admin
Password: letmein
✗ Login failed.
Incorrect password.

Everything this page taught, working together: sized char buffers (not literals, since we write into them), fgets for safe line input with sizeof giving the capacity, strcspn to strip the newline, and strcmp(...) == 0 for comparison — never ==.

The two-branch error message is a small touch worth noticing: telling the user which field was wrong requires comparing both again, which is a realistic tradeoff (a real system would deliberately not reveal which was wrong, for security — worth mentioning if you teach this).


Arrays of Strings (A Preview)

One string is a char array. Multiple strings are an array of char arrays:

main.c
Run Online
char days[7][10] = {
    "Monday", "Tuesday", "Wednesday",
    "Thursday", "Friday", "Saturday", "Sunday"
};
printf("%s\n", days[0]);
// Monday
printf("%c\n", days[3][0]);
// T — first char of Thursday

That's a 2D array of characters — 7 rows, 10 columns each. It works, and it gets its own tutorial next in this cluster along with the char *names[] alternative, which has different trade-offs.


Common Mistakes

  1. Forgetting the '\0' when initializing character by character — everything downstream reads past the end.
  2. Assuming "text" and char arr[] = "text" are the same — one's read-only, the other's yours to modify.
  3. Using == to compare strings — compares addresses. Use strcmp(...) == 0.
  4. Size mistakes — a buffer of 5 can hold a 4-character string, not 5. The terminator needs a slot.
  5. Using char name[] = "Ravi"; for input — no spare capacity, so any longer input overflows. Use a sized buffer.
  6. scanf("%s") without a width limit — overflow. And it stops at spaces.
  7. Using gets — removed from the language for good reason. Use fgets.
  8. Forgetting fgets keeps the newline — strip it with strcspn, or your comparisons and output both get surprised.
  9. Using sizeof after the string is passed to a function — you'll get the pointer size, not the buffer size.
  10. Modifying a string returned by a function or pointed to by a literal — undefined behavior and crashes.

(Suggested image: "10 String Mistakes" checklist graphic — consistent with the series)


Final Thoughts

Strings in C reduce to one sentence: a char array whose end is marked with '\0'. Every quirk follows from that — why the terminator exists, why buffers need the extra slot, why strlen has to walk the data, why literal modifications crash, why fgets is the safe reader.

Carry these four habits with you: size your buffers with room for '\0', prefer char arr[..] over char * for anything you'll modify, read text with fgets and strip the newline, and always compare with strcmp. That's the working core of C string handling, and it covers nearly everything you'll write.

Next in this cluster: strings as arrays in practice — arrays of strings, both the char days[7][10] form and the char *days[7] form, and when each is right.


Runnable C Code Example & Practical Implementation

main.c
Run Online
#include <stdio.h>
int main() {
    // Mutable stack character array
    char mutableStr[] = "Code";
    mutableStr[0] = 'M';
    // Valid: becomes "Mode"
    printf("Modified string: %s\
", mutableStr);
    // Pointer to read-only string literal
    const char *readOnlyStr = "Immutable";
    printf("Read-only string: %s\
", readOnlyStr);
    return 0;
}

Key Takeaways, Best Practices & Memory Safety Rules

Best Practice & Key Takeaways
Always declare string literal pointers as const char *str = "..."; so the compiler prevents accidental write attempts.

Frequently Asked Questions & Technical Interview Insights

Technical Interview & Viva Questions

Frequently asked C programming interview questions
Q

Q: What is the size in bytes of the string literal `"C"`?

A: 2 bytes: 1 byte for 'C' and 1 byte for the implicit trailing null terminator `'\\0'`. (A character literal `'C'` is 1 byte, or `sizeof(int)` in C).
Q

Q: What happens if you modify a string literal via pointer: `char *s = "hello"; s[0] = 'H';`?

A: Segmentation fault / crash (writing to read-only memory segment).

Related Tools, Tutorials & Authoritative References

Recommended Interactive Tools

Authoritative Standards & Citations