Module 4.2

Strings in C

Learn how to work with text data in C using character arrays and strings. Master string declaration, initialization, manipulation functions, and input/output operations to handle textual data effectively in your programs.

40 min read
Beginner
Hands-on Examples
What You'll Learn
  • Understand strings as character arrays
  • Declare and initialize strings
  • Use standard string functions
  • Handle string input and output
  • Avoid common string pitfalls
Contents
01

What Are Strings

In C, strings are not a built-in data type like in many modern languages. Instead, a string is simply an array of characters terminated by a special null character. Understanding this fundamental concept is key to working with text in C.

Imagine you want to store the word "Hello" in your computer's memory. In languages like Python or JavaScript, you simply write message = "Hello" and the language handles everything for you. But C is different. C shows you exactly how text is stored in memory, giving you complete control but also more responsibility.

In C, every string is stored as a sequence of characters placed one after another in memory, like beads on a necklace. Each character occupies exactly one byte. But here is the crucial part: how does the computer know where the string ends? That is where the null terminator comes in.

Beginner Tip: Think of a C string like a train of characters with a special "stop" car at the end. Each train car holds one letter, and the stop car (null character) tells everyone "this is where the string ends!" Without that stop car, nobody knows where to stop reading.

Why Does This Matter?

Memory is just bytes. Your computer does not know what is a string and what is a number. Memory is just a long sequence of bytes.

Strings need boundaries. Without a marker, functions like printf would not know when to stop reading your string.

The null terminator solves this. It is the universal "stop sign" that says "the string ends here."

What You Will Learn

How strings are stored in memory

The role of the null terminator

Difference between characters and strings

Common beginner mistakes to avoid

The Null Terminator: The Heart of C Strings

The null terminator (written as '\0') is the special character that marks the end of every string in C. It has an ASCII value of 0 and is different from the digit '0' (which has ASCII value 48). This tiny character is absolutely essential - without it, string functions would not know where your string ends.

Here is a simple way to think about it: Imagine you are reading a book, but the book has no period at the end of sentences. You would not know when one sentence ends and the next begins! The null terminator is like that period - it tells the computer "stop reading here."

Important Distinction: The null terminator '\0' (ASCII value 0) is NOT the same as the character '0' (ASCII value 48). The digit zero is what you type to represent the number zero in text. The null terminator is an invisible "stop" signal.
Concept

C String

A C string is a sequence of characters stored in consecutive memory locations, where the last character is always the null terminator '\0'. This null character acts like a "stop sign" that tells all string-handling functions where the string data ends.

In simple terms: A C string = characters + null terminator at the end.

Because strings are just character arrays, C has no way to "know" the length of a string without counting characters until it hits the null terminator. This is why the strlen() function exists - it walks through the array counting until it finds '\0'.

Key insight: The null terminator takes up one byte of storage. A string with 5 visible characters ("Hello") requires 6 bytes of memory - 5 for the letters plus 1 for the '\0'. Always remember: bytes needed = character count + 1.

How Strings Are Stored in Memory

Let us peek inside your computer's memory to see exactly how a string is stored. When you create a string like "Hello", C stores each character in consecutive memory locations, one byte per character, followed by the null terminator.

Visualize it like mailboxes on a street: Each mailbox (memory location) holds exactly one letter. The mailboxes are numbered (indexed) starting from 0. The last mailbox contains a special "end of mail" marker (the null terminator).

Index 0 1 2 3 4 5
Character 'H' 'e' 'l' 'l' 'o' '\0'
ASCII Value 72 101 108 108 111 0

Memory layout of the string "Hello" - notice the null terminator at index 5

#include <stdio.h>

int main() {
    char greeting[] = "Hello";
    
    // Print each character with its index and ASCII value
    for (int i = 0; i <= 5; i++) {
        printf("Index %d: '%c' (ASCII: %d)\n", i, greeting[i], greeting[i]);
    }
    
    return 0;
}

// Output:
// Index 0: 'H' (ASCII: 72)
// Index 1: 'e' (ASCII: 101)
// Index 2: 'l' (ASCII: 108)
// Index 3: 'l' (ASCII: 108)
// Index 4: 'o' (ASCII: 111)
// Index 5: '' (ASCII: 0)   <-- The null terminator!

We loop from index 0 to 5 (inclusive) to see all 6 bytes. The character at index 5 prints as empty because '\0' has no visible representation, but its ASCII value of 0 confirms it is the null terminator.

String vs Character Array

This is a crucial distinction that confuses many beginners: every string is a character array, but not every character array is a string!

What makes a character array a valid string? The presence of the null terminator. Without it, you just have a bunch of characters sitting in memory - not a string that C functions can safely work with.

Why does this matter? String functions like printf("%s"), strlen(), and strcpy() all depend on finding that null terminator. If it is missing, they will keep reading past your array into random memory until they happen to find a zero byte somewhere - leading to garbage output, crashes, or security vulnerabilities.

Valid String
// This is a proper string
char str1[] = "Hi";  // Compiler adds '\0'
// Memory: ['H', 'i', '\0']

char str2[4] = {'H', 'i', '\0'};  // Manual '\0'
// Memory: ['H', 'i', '\0', ?]

printf("%s\n", str1);  // Works: "Hi"
printf("%zu\n", strlen(str1));  // Works: 2
NOT a Valid String
// Missing null terminator!
char chars[2] = {'H', 'i'};  // No '\0'!
// Memory: ['H', 'i']

// DANGER: These will read past the array!
printf("%s\n", chars);    // Undefined behavior!
strlen(chars);            // Undefined behavior!

// Will keep reading memory until it
// happens to find a zero byte
Undefined Behavior: Using string functions on character arrays without null terminators causes undefined behavior. The program might print garbage, crash, or appear to work but corrupt data. Always ensure your strings are properly terminated!

Character Literal vs String Literal

C uses different quote marks for single characters and strings. This is a common source of confusion and bugs for beginners, so pay close attention!

The golden rule:

  • Single quotes 'A' = exactly ONE character (just the letter, nothing else)
  • Double quotes "A" = a STRING (the letter + invisible null terminator)

Why the difference matters: 'A' takes 1 byte of memory. "A" takes 2 bytes (the letter 'A' plus the null terminator '\0'). They are stored differently and used for different purposes.

Character Literal

Uses single quotes: 'A'

Represents exactly one character

Type is int (stores ASCII value)

'A' is the integer 65

String Literal

Uses double quotes: "A"

Always includes null terminator

Type is char* (pointer to char)

"A" is 2 bytes: 'A' and '\0'

#include <stdio.h>

int main() {
    char letter = 'A';        // Single character, 1 byte
    char str[] = "A";         // String, 2 bytes ('A' + '\0')
    
    printf("sizeof(letter): %zu\n", sizeof(letter));  // 1
    printf("sizeof(str): %zu\n", sizeof(str));        // 2
    
    // Character arithmetic works because chars are integers
    printf("'A' + 1 = '%c'\n", 'A' + 1);  // 'B'
    printf("'a' - 32 = '%c'\n", 'a' - 32);  // 'A' (uppercase)
    
    return 0;
}

Line-by-line breakdown: We declare a single character letter (1 byte) and a string str (2 bytes including null). The sizeof operator reveals the difference. Character arithmetic works because characters are stored as their ASCII integer values - adding 1 to 'A' gives 'B', and subtracting 32 from lowercase converts to uppercase.

Common Mistake: Writing 'Hello' with single quotes is an error! Single quotes are only for individual characters. Use double quotes for strings: "Hello".

Practice Questions: What Are Strings

Test your understanding of C strings and the null terminator.

Question: How many bytes does the string "World" occupy in memory?

View Solution

6 bytes

The string has 5 visible characters ('W', 'o', 'r', 'l', 'd') plus 1 byte for the null terminator ('\0').

char str[] = "World";
printf("%zu\n", sizeof(str));  // 6

Question: Which of these creates a valid C string?

char a[3] = {'C', 'a', 't'};
char b[4] = {'C', 'a', 't', '\0'};
char c[3] = "Cat";
View Solution

Only b is a valid string.

  • a: No null terminator - NOT a valid string
  • b: Has null terminator - valid string
  • c: Array too small! "Cat" needs 4 bytes but only 3 are allocated. This may compile but causes issues.

Question: What will this code print?

char str[10] = "Hi";
printf("%zu %zu\n", sizeof(str), strlen(str));
View Solution

Output: 10 2

  • sizeof(str) returns 10 - the total array size in bytes
  • strlen(str) returns 2 - the number of characters before '\0'

Remember: sizeof gives array size, strlen gives string length!

02

Declaration and Initialization

There are several ways to declare and initialize strings in C. Each method has its use cases, and understanding the differences will help you choose the right approach for your needs.

Before we dive in, let us understand what "declaring" and "initializing" mean:

  • Declaring means telling C: "I need a place in memory to store a string"
  • Initializing means filling that memory with actual characters

Think of it like building a house: declaring is like constructing the empty rooms, and initializing is like moving furniture into those rooms. You can do both at once, or build first and furnish later.

Why Multiple Methods? Different situations call for different approaches. Sometimes you know the exact text at compile time, sometimes you need to build strings at runtime, and sometimes you just need a buffer to read user input. Each method we cover handles these scenarios differently.

Method 1: Using String Literals with Character Arrays

The most common and convenient way to create a string is to initialize a character array with a string literal (text in double quotes). This is the method you will use 90% of the time.

How it works step by step:

  1. You write text in double quotes: "Hello, World!"
  2. The compiler counts the characters (13 letters + 1 null = 14 bytes needed)
  3. C creates an array of exactly that size
  4. Each character is copied into the array
  5. The null terminator '\0' is automatically added at the end

The beauty of this method is that you do not need to count characters or add the null terminator yourself - the compiler handles everything!

#include <stdio.h>

int main() {
    // Let compiler calculate size (recommended for initialization)
    char greeting[] = "Hello, World!";
    
    // Explicitly specify size (must include room for '\0')
    char name[20] = "Alice";
    
    // Print strings and their sizes
    printf("greeting: \"%s\" (size: %zu)\n", greeting, sizeof(greeting));
    printf("name: \"%s\" (size: %zu)\n", name, sizeof(name));
    
    // Strings in arrays are MODIFIABLE
    greeting[0] = 'J';  // Change 'H' to 'J'
    printf("Modified: %s\n", greeting);  // "Jello, World!"
    
    return 0;
}

Using [] without a size lets the compiler count characters. The array is stored on the stack and can be modified. With explicit size, extra bytes are zero-initialized.

Advantages

  • String contents can be modified
  • Compiler handles null terminator
  • Simple and intuitive syntax
  • Memory is on the stack (fast access)

Considerations

  • Cannot grow beyond initial size
  • Stack memory is limited
  • Must ensure buffer is large enough
  • Cannot return stack arrays from functions

Method 2: Character-by-Character Initialization

You can initialize a character array by listing each character individually. This gives you explicit control but requires you to manually include the null terminator!

#include <stdio.h>

int main() {
    // Correct: includes null terminator
    char word1[] = {'C', 'o', 'd', 'e', '\0'};
    
    // Also correct: extra space initialized to zero
    char word2[10] = {'C', 'o', 'd', 'e', '\0'};
    
    // WRONG: missing null terminator - NOT a valid string!
    char word3[] = {'C', 'o', 'd', 'e'};  // Just a char array
    
    printf("word1: %s\n", word1);  // Safe: "Code"
    printf("word2: %s\n", word2);  // Safe: "Code"
    // printf("%s\n", word3);      // DANGER: undefined behavior!
    
    printf("sizeof word1: %zu\n", sizeof(word1));  // 5 bytes
    printf("sizeof word3: %zu\n", sizeof(word3));  // 4 bytes (no '\0')
    
    return 0;
}

What this demonstrates: word1 and word2 are valid strings because they include '\0'. word3 is just a character array without a null terminator - using it with printf("%s") or strlen() causes undefined behavior. Notice sizeof(word1) returns 5 (4 chars + null) while sizeof(word3) returns only 4.

Remember: When using character-by-character initialization, you MUST include '\0' as the last element, or the result is NOT a valid string!

Method 3: Pointer to String Literal

You can also use a character pointer to point to a string literal. However, this creates a read-only string because string literals are stored in a special read-only section of memory.

#include <stdio.h>

int main() {
    // Pointer to string literal (READ-ONLY!)
    const char *message = "Hello!";
    
    printf("%s\n", message);  // Works fine
    
    // message[0] = 'J';  // CRASH! Undefined behavior!
    
    // But you CAN point to a different string
    message = "Goodbye!";
    printf("%s\n", message);  // "Goodbye!"
    
    // Compare with array version
    char arr[] = "Hello!";
    arr[0] = 'J';  // This is FINE - array is modifiable
    printf("%s\n", arr);  // "Jello!"
    
    return 0;
}

Key takeaways: With const char *message, the pointer can be changed to point to a different string, but the string content itself cannot be modified. With char arr[], the array contents are modifiable because the string literal is copied into stack memory. This is why we can change arr[0] but not message[0].

Important Distinction

Array vs Pointer Initialization

char str[] = "Hello";

  • Creates array on stack
  • Copies literal into array
  • String IS modifiable
  • sizeof(str) = 6

char *str = "Hello";

  • Creates pointer on stack
  • Points to read-only literal
  • String is NOT modifiable
  • sizeof(str) = 4 or 8 (pointer size)

Best Practice: Always use const char * for pointers to string literals. This makes the read-only nature explicit and helps the compiler catch mistakes.

Common Declaration Mistakes

Here are common mistakes beginners make when declaring strings, and how to fix them:

Wrong
// Mistake 1: Array too small
char name[5] = "Alice";  // Needs 6 bytes!

// Mistake 2: Missing null terminator
char vowels[] = {'a', 'e', 'i', 'o', 'u'};

// Mistake 3: Assigning after declaration
char city[20];
city = "London";  // ERROR: cannot assign
Correct
// Fix 1: Make array big enough
char name[6] = "Alice";  // or name[]

// Fix 2: Include null terminator
char vowels[] = {'a','e','i','o','u','\0'};

// Fix 3: Use strcpy after declaration
char city[20];
strcpy(city, "London");  // Works!

Empty Strings and Initialization

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

int main() {
    // Empty string: just the null terminator
    char empty[] = "";
    printf("Empty string length: %zu\n", strlen(empty));  // 0
    printf("Empty string size: %zu\n", sizeof(empty));    // 1 (just '\0')
    
    // Uninitialized vs zero-initialized
    char buffer1[50];           // Contains garbage!
    char buffer2[50] = "";      // First byte is '\0', rest are 0
    char buffer3[50] = {0};     // All 50 bytes are 0
    
    // buffer1 is dangerous to use with string functions
    printf("buffer2: \"%s\"\n", buffer2);  // Safe, prints nothing
    printf("buffer3: \"%s\"\n", buffer3);  // Safe, prints nothing
    
    return 0;
}

Understanding initialization: An empty string "" still takes 1 byte for the null terminator. buffer1 contains garbage and is dangerous to use. buffer2 and buffer3 are both safely initialized - = "" sets the first byte to '\0' and zeros the rest, while = {0} zeros all bytes.

Pro Tip: Always initialize string buffers. Using char buf[50] = ""; or char buf[50] = {0}; ensures the buffer contains a valid empty string and prevents garbage data issues.

Practice Questions: Declaration and Initialization

Test your understanding of string declaration methods.

Question: You need to store the word "Code" and later change the first letter to 'M'. Which declaration should you use?

A) const char *word = "Code";

B) char word[] = "Code";

C) char *word = "Code";

View Solution

B) char word[] = "Code";

This creates a modifiable array on the stack. Options A and C point to read-only string literals, so modifying them would cause undefined behavior.

Question: This code has bugs. Find and fix them.

char country[5];
country = "India";
printf("%s\n", country);
View Solution

Two problems:

  1. Cannot assign to arrays after declaration. Use strcpy().
  2. "India" needs 6 bytes (5 letters + null), but array only has 5.
char country[6];  // or larger
strcpy(country, "India");
printf("%s\n", country);

Question: What does each sizeof return? (Assume 64-bit system)

char arr[] = "Test";
char *ptr = "Test";
char fixed[50] = "Test";

printf("%zu\n", sizeof(arr));
printf("%zu\n", sizeof(ptr));
printf("%zu\n", sizeof(fixed));
View Solution

Output:

  • sizeof(arr) = 5 (4 chars + 1 null terminator)
  • sizeof(ptr) = 8 (pointer size on 64-bit systems)
  • sizeof(fixed) = 50 (declared array size, not string length)

Key insight: sizeof returns the size of the variable itself, not the string length. Use strlen() for string length!

03

String Functions

The C standard library provides a rich set of functions for string manipulation through the string.h header. These functions handle common operations like copying, comparing, concatenating, and searching strings.

Since C does not have built-in string operations like + for concatenation or == for comparison, we need to use library functions instead. Think of string.h as a toolbox full of ready-made tools for working with strings.

Here is what you cannot do in C (unlike other languages):

Cannot do in C
// These do NOT work in C!
char str1[] = "Hello";
char str2[] = "World";

// NO: Cannot use + to join strings
char result[] = str1 + str2;

// NO: Cannot use == to compare
if (str1 == str2) { ... }
Use functions instead
// Use string.h functions!
#include <string.h>

// Use strcat() to join strings
strcat(str1, str2);

// Use strcmp() to compare
if (strcmp(str1, str2) == 0) { ... }

To use these functions, include the <string.h> header at the top of your program. Let us explore the most commonly used string functions and learn how to use them safely.

Include Required: All functions in this section require #include <string.h> at the top of your source file.

strlen() - Get String Length

The strlen() function counts the number of characters in a string, NOT including the null terminator. It is like counting letters in a word.

How strlen() works internally: It starts at the beginning of the string and walks through memory, counting each character until it finds the null terminator '\0'. Then it returns the count. This is why strings without null terminators are dangerous - strlen() would keep walking through memory forever!

Function

strlen()

Syntax: size_t strlen(const char *str);

Returns: The number of characters before the null terminator.

Note: Returns size_t (unsigned), so use %zu for printing.

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

int main() {
    char text[] = "Hello";
    
    size_t length = strlen(text);
    printf("Length of '%s': %zu\n", text, length);  // 5
    
    // strlen vs sizeof
    printf("strlen: %zu\n", strlen(text));   // 5 (characters only)
    printf("sizeof: %zu\n", sizeof(text));   // 6 (includes '\0')
    
    // Empty string
    char empty[] = "";
    printf("Empty string length: %zu\n", strlen(empty));  // 0
    
    return 0;
}

Key insight: strlen() returns 5 for "Hello" (characters only), while sizeof() returns 6 (includes the null terminator). For an empty string "", strlen returns 0 because there are no characters before the null. Always use %zu to print size_t values returned by strlen.

strcpy() and strncpy() - Copy Strings

These functions copy strings from one location to another. Why do we need them? Because you cannot copy strings using the assignment operator (=) in C!

Why not just use =? When you write str2 = str1, you are not copying the string. You are just copying the memory address (pointer). Both variables now point to the same string, and changes to one affect the other. To make a true copy, you need these functions.

Real-world analogy: Think of strcpy() like a photocopier. It makes an exact duplicate of the original document (source string) onto a new piece of paper (destination). strncpy() is like a copier that only copies a certain number of pages - useful when your destination paper stack is limited.

strcpy(dest, src)

Copies entire string including null terminator

Danger: No bounds checking!

If dest is too small, buffer overflow occurs

strncpy(dest, src, n)

Copies at most n characters

Caution: May not add null terminator!

Must manually null-terminate if src is longer than n

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

int main() {
    char source[] = "Programming";
    char dest1[20];
    char dest2[5];
    
    // strcpy - copies everything
    strcpy(dest1, source);
    printf("dest1: %s\n", dest1);  // "Programming"
    
    // strncpy - copies at most n characters
    strncpy(dest2, source, 4);
    dest2[4] = '\0';  // IMPORTANT: manually null-terminate!
    printf("dest2: %s\n", dest2);  // "Prog"
    
    // Safe pattern for strncpy
    char buffer[10];
    strncpy(buffer, "LongString", sizeof(buffer) - 1);
    buffer[sizeof(buffer) - 1] = '\0';  // Always null-terminate
    printf("buffer: %s\n", buffer);  // "LongStrin"
    
    return 0;
}

Step by step: strcpy(dest1, source) copies the entire string. strncpy(dest2, source, 4) copies only 4 characters, but does NOT add a null terminator when the source is longer than n - so we must add it manually with dest2[4] = '\0'. The safe pattern always copies sizeof(buffer) - 1 chars and null-terminates the last byte.

Buffer Overflow Warning: Never use strcpy() with user input or unknown-length strings. Always use strncpy() with proper size limits, or even better, use snprintf() for safe string operations.

strcat() and strncat() - Concatenate Strings

"Concatenate" means to join two strings together, end-to-end. The strcat() function appends (adds) one string to the end of another.

How it works: strcat() finds the null terminator in the destination string, removes it, copies the source string there, and adds a new null terminator at the very end.

Visual example:

  • Before: dest = "Hello\0" and src = " World"
  • strcat(dest, src) finds the '\0' in dest, overwrites it with ' ', and continues copying
  • After: dest = "Hello World\0"
Critical Requirement: The destination buffer MUST have enough space to hold BOTH strings plus the null terminator. If dest can hold 15 characters and already contains "Hello" (5 chars), you can only safely append 9 more characters (5 + 9 + 1 = 15).
#include <stdio.h>
#include <string.h>

int main() {
    char greeting[50] = "Hello";  // Must have extra space!
    char name[] = " World";
    
    // strcat - appends entire string
    strcat(greeting, name);
    printf("%s\n", greeting);  // "Hello World"
    
    // Chaining concatenations
    strcat(greeting, "!");
    printf("%s\n", greeting);  // "Hello World!"
    
    // strncat - appends at most n characters
    char buffer[20] = "Hi";
    strncat(buffer, " there, friend!", 6);  // Adds " there" (6 chars)
    printf("%s\n", buffer);  // "Hi there"
    
    return 0;
}

How it works: strcat(greeting, name) finds the null in "Hello", overwrites it with ' ' from " World", and continues copying until the new null. We can chain multiple strcat calls. strncat limits how many characters are appended - here it adds only 6 characters from the source string.

Common Mistake: Forgetting that the destination needs enough space for BOTH strings plus the null terminator. If dest has 10 bytes and contains "Hello" (5 chars), you can only safely append 4 more characters!

strcmp() and strncmp() - Compare Strings

The strcmp() function compares two strings and tells you if they are equal or which one comes first in dictionary (lexicographic) order.

Why do we need this? In C, you cannot compare strings using ==. If you write str1 == str2, you are comparing memory addresses (where the strings are stored), not the actual content. Two strings with identical characters stored at different locations would compare as "not equal"!

How strcmp() works: It compares strings character by character, using ASCII values. It stops at the first difference or when it reaches a null terminator.

Understanding the return values:

Return Value Meaning Example
< 0 str1 comes before str2 strcmp("apple", "banana") returns negative
0 str1 equals str2 strcmp("hello", "hello") returns 0
> 0 str1 comes after str2 strcmp("zebra", "apple") returns positive
#include <stdio.h>
#include <string.h>

int main() {
    char password[] = "secret123";
    char input[] = "secret123";
    char wrong[] = "password";
    
    // Exact comparison
    if (strcmp(password, input) == 0) {
        printf("Password correct!\n");
    }
    
    if (strcmp(password, wrong) != 0) {
        printf("Password incorrect!\n");
    }
    
    // Compare only first n characters
    if (strncmp("Hello World", "Hello Earth", 5) == 0) {
        printf("First 5 characters match!\n");  // This prints
    }
    
    // Sorting comparison
    printf("apple vs banana: %d\n", strcmp("apple", "banana"));  // Negative
    printf("cat vs cat: %d\n", strcmp("cat", "cat"));            // 0
    printf("dog vs cat: %d\n", strcmp("dog", "cat"));            // Positive
    
    return 0;
}

Understanding the output: strcmp returns 0 when strings match exactly. For password checking, always compare to 0 explicitly: if (strcmp(a, b) == 0). strncmp compares only the first n characters - useful for checking prefixes like "Hello World" and "Hello Earth" sharing "Hello".

Never use == for strings! Writing str1 == str2 compares memory addresses, not contents. Always use strcmp() for string comparison.

strchr() and strstr() - Search in Strings

These functions help you find characters or substrings within a string. They are like the "Find" feature in a text editor.

How they work:

  • strchr(str, 'x') - Finds the first occurrence of character 'x' in str
  • strrchr(str, 'x') - Finds the LAST occurrence of character 'x' in str
  • strstr(str, "word") - Finds the first occurrence of substring "word" in str

What they return: A pointer to where the match was found, or NULL if nothing was found. This pointer points directly into the original string, so you can use it to access everything from that position onward.

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

int main() {
    char text[] = "Hello, World!";
    
    // strchr - find a character
    char *comma = strchr(text, ',');
    if (comma != NULL) {
        printf("Found comma at position: %ld\n", comma - text);  // 5
        printf("Text after comma: %s\n", comma);  // ", World!"
    }
    
    // strrchr - find LAST occurrence of character
    char *lastL = strrchr(text, 'l');
    printf("Last 'l' at position: %ld\n", lastL - text);  // 10
    
    // strstr - find a substring
    char *world = strstr(text, "World");
    if (world != NULL) {
        printf("Found 'World' at position: %ld\n", world - text);  // 7
    }
    
    // Not found returns NULL
    if (strstr(text, "xyz") == NULL) {
        printf("'xyz' not found\n");
    }
    
    return 0;
}

Reading the results: strchr returns a pointer to the found character. comma - text calculates the position by subtracting pointers (pointer arithmetic). Printing from the returned pointer prints everything from that position onward. Always check for NULL before using the result - it means "not found".

String Functions Quick Reference

Function Purpose Returns
strlen(s) Get string length Number of characters (size_t)
strcpy(dest, src) Copy string Pointer to dest
strncpy(dest, src, n) Copy at most n characters Pointer to dest
strcat(dest, src) Concatenate strings Pointer to dest
strncat(dest, src, n) Concatenate at most n characters Pointer to dest
strcmp(s1, s2) Compare strings <0, 0, or >0
strncmp(s1, s2, n) Compare first n characters <0, 0, or >0
strchr(s, c) Find first occurrence of char Pointer or NULL
strrchr(s, c) Find last occurrence of char Pointer or NULL
strstr(s1, s2) Find substring Pointer or NULL

Practice Questions: String Functions

Test your understanding of C string functions.

Question: Write code to combine "Good " and "Morning" into a single string.

View Solution
char result[20] = "Good ";
strcat(result, "Morning");
printf("%s\n", result);  // "Good Morning"

Key: Initialize result with the first string and enough space, then use strcat.

Question: Safely copy the string "International" into a buffer of only 8 characters, ensuring proper null termination.

View Solution
char buffer[8];
strncpy(buffer, "International", 7);  // Copy 7 chars max
buffer[7] = '\0';  // Ensure null termination
printf("%s\n", buffer);  // "Interna"

We copy sizeof(buffer)-1 characters to leave room for the null terminator.

Question: Write a function that counts how many times a character appears in a string using strchr().

View Solution
int countChar(const char *str, char c) {
    int count = 0;
    const char *ptr = str;
    
    while ((ptr = strchr(ptr, c)) != NULL) {
        count++;
        ptr++;  // Move past found character
    }
    
    return count;
}

// Usage
printf("%d\n", countChar("Mississippi", 's'));  // 4
04

String Input/Output

Reading strings from users and displaying them requires careful handling in C. Understanding the different input functions and their behaviors is essential to avoid buffer overflows and other common pitfalls.

Almost every program needs to communicate with users - whether it is asking for their name, reading a command, or displaying results. This is called Input/Output (I/O). In C, string I/O requires extra care because:

  • Output is straightforward - You control what gets displayed
  • Input is dangerous - You do NOT control what users type!

Imagine you create a 10-character buffer for a username, but a user types 50 characters. What happens to those extra 40 characters? They overflow into adjacent memory, potentially corrupting data or crashing your program. This is called a buffer overflow, and it is one of the most common security vulnerabilities in software.

Security Note: Buffer overflow attacks have been responsible for some of the most devastating security breaches in computing history. Learning safe string input habits now will make you a better, more security-conscious programmer.

Output Functions: Displaying Strings

Displaying strings is the easier part of string I/O. You have complete control over what gets shown because you are providing the data. Here are the three main ways to output strings:

  • printf("%s", str) - Most flexible, supports formatting options
  • puts(str) - Simplest, automatically adds a newline at the end
  • putchar(c) - Prints one character at a time (useful in loops)
#include <stdio.h>

int main() {
    char message[] = "Hello, C!";
    
    // printf with %s - most versatile
    printf("Message: %s\n", message);
    
    // puts - prints string with automatic newline
    puts(message);  // Adds '\n' at the end
    
    // printf with formatting options
    printf("[%-15s]\n", message);   // Left-aligned, 15 chars wide
    printf("[%15s]\n", message);    // Right-aligned, 15 chars wide
    printf("[%.5s]\n", message);    // Print only first 5 chars
    
    // Print character by character
    for (int i = 0; message[i] != '\0'; i++) {
        putchar(message[i]);
    }
    putchar('\n');
    
    return 0;
}

Output formatting explained: %-15s left-aligns in 15-character width, %15s right-aligns. %.5s prints only the first 5 characters (precision). The loop with putchar shows how you can print character by character - notice we check for '\0' to know when to stop.

printf()

Use %s format specifier

Supports width and precision

Does NOT add newline

puts()

Prints string as-is

Automatically adds newline

Simple but limited

putchar()

Prints single character

Useful for loops

Character by character output

Input Functions: Reading Strings

Reading strings from users is more dangerous than output because you cannot control how much data the user enters. Using the wrong function can lead to buffer overflows and security vulnerabilities.

scanf() with %s - Simple but Limited

scanf("%s", ...) reads a string until it encounters whitespace (space, tab, or newline). This makes it unsuitable for reading full sentences or names with spaces.

Step by Step

How scanf("%s") Works

1

Skip Whitespace

Ignores any leading spaces, tabs, or newlines before the actual input

2

Read Characters

Reads characters one by one and stores them in your buffer

3

Stop at Space

Stops immediately when it encounters a space, tab, or newline

4

Add Null

Automatically adds '\\0' at the end to make it a valid string

Example Scenario

User types: John Smith

scanf captures: "John" Left in buffer: "Smith"

The space stops reading. "Smith" waits for the next scanf call.

#include <stdio.h>

int main() {
    char word[20];
    
    printf("Enter a word: ");
    scanf("%s", word);  // No & needed - array name is already a pointer
    
    printf("You entered: %s\n", word);
    
    // If user types "Hello World", only "Hello" is stored!
    
    return 0;
}

Important notes: No & is needed before word because an array name already acts as a pointer to its first element. If the user types "Hello World", scanf only captures "Hello" - the space stops reading. "World" remains in the input buffer for the next read operation.

Dangerous
char buffer[10];
scanf("%s", buffer);  // No size limit!
// User can overflow buffer
Safer
char buffer[10];
scanf("%9s", buffer);  // Max 9 chars + '\0'
// Cannot overflow 10-byte buffer

gets() - NEVER USE THIS!

DEPRECATED! The gets() function was removed from the C standard (C11) because it has no way to prevent buffer overflows. It reads until newline with no size checking. Never use gets() in any code.

fgets() - The Safe Choice

fgets() is the recommended function for reading strings because it lets you specify the maximum number of characters to read. This prevents buffer overflows.

How fgets() protects you:

  1. You tell it: "Read into this buffer, but NEVER more than N characters"
  2. It reads characters until: newline, EOF, or reaching the limit (whichever comes first)
  3. It ALWAYS adds a null terminator (guaranteed safe string)
  4. If there is room, it includes the newline character in the result

The one quirk: fgets() keeps the newline character ('\n') in the string if the user presses Enter and there is room. So if a user types "Alice" and presses Enter, you get "Alice\n\0". Most programs remove this newline immediately after reading.

Recommended Function

fgets()

Syntax: char *fgets(char *str, int n, FILE *stream);

Parameters:

  • str: Buffer to store the string
  • n: Maximum characters to read (including null terminator)
  • stream: Input stream (use stdin for keyboard)

Returns: Pointer to str on success, NULL on failure

Note: fgets() keeps the newline character in the string if there is room!

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

int main() {
    char name[50];
    
    printf("Enter your full name: ");
    fgets(name, sizeof(name), stdin);
    
    // Remove trailing newline if present
    size_t len = strlen(name);
    if (len > 0 && name[len - 1] == '\n') {
        name[len - 1] = '\0';
    }
    
    printf("Hello, %s!\n", name);
    
    return 0;
}

Breaking it down: fgets(name, sizeof(name), stdin) reads up to 49 characters (leaving room for null) from keyboard input. The newline removal code checks if the last character is '\n' and replaces it with '\0'. This is the recommended pattern for all user string input in C.

Pro Tip: Always remove the trailing newline from fgets() input. A common pattern is: name[strcspn(name, "\n")] = '\0'; which finds the newline position and replaces it with null.

Input Functions Comparison

Function Reads Spaces Size Limit Keeps Newline Safety
scanf("%s") No Only with width specifier No Medium
gets() Yes None No UNSAFE - Never use!
fgets() Yes Yes Yes SAFE - Recommended

Practice Questions: String I/O

Test your understanding of string input and output.

Question: Write code to safely read a user's full name (which may contain spaces) into a 100-character buffer.

View Solution
char fullName[100];
printf("Enter your full name: ");
fgets(fullName, sizeof(fullName), stdin);

// Remove newline
fullName[strcspn(fullName, "\n")] = '\0';

printf("Name: %s\n", fullName);

Question: Explain what is wrong with this code:

char password[8];
printf("Enter password: ");
scanf("%s", password);
View Solution

Two problems:

  1. No size limit on scanf - if user enters more than 7 characters, buffer overflow occurs
  2. Password will be visible on screen (security issue, though not related to strings)

Fix: Use scanf("%7s", password) or better, use fgets()

05

Arrays of Strings

Often you need to work with multiple strings at once, like a list of names or command-line arguments. C provides two main approaches: 2D character arrays and arrays of pointers.

Think about situations where you need to store multiple strings:

  • A list of student names
  • Days of the week
  • Menu options for a program
  • Command-line arguments passed to your program
  • Words in a sentence you want to analyze

In C, we cannot simply say "array of strings" because strings themselves are arrays! So we end up with an array of arrays, which can be created in two different ways, each with its own trade-offs.

Simple Analogy: Think of a 2D character array like a spreadsheet with fixed-width columns - every row has the same number of cells, even if some cells are empty. An array of pointers is like a list of bookmarks - each bookmark points to a string that can be any length.

Method 1: 2D Character Array

A 2D character array is like a table or grid where each row holds one string. You must specify the maximum length for all strings when you create the array.

How to read the declaration char fruits[5][20]:

  • [5] - Create 5 rows (5 strings)
  • [20] - Each row has 20 columns (max 19 characters + null terminator)
  • Total memory: 5 x 20 = 100 bytes, always, regardless of actual string lengths

The downside: If you store the word "Apple" (6 bytes including null), the remaining 14 bytes in that row are wasted. This is the price of simplicity - fixed size means predictable memory usage but potential waste.

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

int main() {
    // 5 strings, each up to 19 characters + null
    char fruits[5][20] = {
        "Apple",
        "Banana",
        "Cherry",
        "Date",
        "Elderberry"
    };
    
    // Print all fruits
    printf("Fruit list:\n");
    for (int i = 0; i < 5; i++) {
        printf("  %d. %s\n", i + 1, fruits[i]);
    }
    
    // Modify a string (modifiable!)
    strcpy(fruits[0], "Apricot");
    printf("\nFirst fruit changed to: %s\n", fruits[0]);
    
    // Access individual characters
    printf("First letter of second fruit: %c\n", fruits[1][0]);  // 'B'
    
    return 0;
}

Understanding the syntax: fruits[5][20] creates 5 rows of 20 characters each. fruits[i] accesses the i-th string, fruits[i][j] accesses the j-th character of that string. We use strcpy to modify strings because direct assignment after declaration does not work in C.

Advantages

  • Strings are modifiable
  • Simple contiguous memory layout
  • Easy to understand and debug
  • No pointer management needed

Disadvantages

  • Wastes memory for short strings
  • All strings limited to same max length
  • Cannot resize after declaration
  • Fixed number of strings

Memory Layout of 2D Array

The memory for a 2D array is laid out in one contiguous (unbroken) block. All 30 bytes are neighbors in memory, regardless of how long your actual strings are.

With char names[3][10]:

  • Row 0 occupies bytes 0-9
  • Row 1 occupies bytes 10-19
  • Row 2 occupies bytes 20-29
  • Total: exactly 30 bytes, always allocated
Row Characters (10 bytes each)
names[0] 'T''o''m''\0' 000 000
names[1] 'S''a''m''\0' 000 000
names[2] 'E''l''i''z''a''b''e''t''h''\0'

Notice how "Tom" and "Sam" waste 6 bytes each (shown in gray), while "Elizabeth" uses all available space.

Method 2: Array of Character Pointers

An array of pointers is fundamentally different. Instead of storing the actual strings, it stores the addresses (memory locations) of where each string lives.

How to read the declaration const char *days[7]:

  • * - Each element is a pointer (stores an address)
  • [7] - There are 7 pointers
  • Each pointer can point to a string of ANY length
  • The strings themselves are stored elsewhere in memory

Think of it like this: Imagine you have a contact list on your phone. The list itself is small - it just stores names and phone numbers (addresses). The actual people (strings) could be anywhere in the world. The list does not contain the people; it just tells you how to find them.

Why const? When we point to string literals (text in double quotes), those literals are stored in read-only memory. We use const to remind ourselves: "I can look at these strings, but I cannot modify them."
#include <stdio.h>

int main() {
    // Array of pointers to string literals (READ-ONLY!)
    const char *days[] = {
        "Monday",
        "Tuesday",
        "Wednesday",
        "Thursday",
        "Friday",
        "Saturday",
        "Sunday"
    };
    
    int numDays = sizeof(days) / sizeof(days[0]);
    
    printf("Days of the week:\n");
    for (int i = 0; i < numDays; i++) {
        printf("  %s (%zu chars)\n", days[i], strlen(days[i]));
    }
    
    // You can change which string a pointer points to
    days[0] = "Lunes";  // Now points to different literal
    printf("\nFirst day is now: %s\n", days[0]);
    
    // But cannot modify the string content itself!
    // days[0][0] = 'X';  // CRASH! String literal is read-only
    
    return 0;
}

What is happening: sizeof(days) / sizeof(days[0]) calculates the number of elements (7 pointers). We can reassign days[0] to point to a different string literal, but we cannot modify the characters of the string because string literals are stored in read-only memory.

Key Concept

Pointer Array Memory Layout

With const char *names[3], you have an array of 3 pointers (24 bytes on 64-bit). Each pointer can point to a string of any length stored elsewhere in memory.

The pointer array:

  • names[0] points to "Tom" (3 bytes + '\0')
  • names[1] points to "Sam" (3 bytes + '\0')
  • names[2] points to "Elizabeth" (9 bytes + '\0')

Total memory:

  • Pointer array: 24 bytes
  • Strings: 4 + 4 + 10 = 18 bytes
  • Total: 42 bytes (vs 30 for 2D array)

Making Pointer Arrays Modifiable

We learned that pointer arrays pointing to string literals are read-only. But what if you need to modify the strings? The solution is to allocate your own memory using malloc().

What is malloc()? It is a function that asks the operating system for a chunk of memory. You specify how many bytes you need, and it returns a pointer to that memory. Unlike string literals, this memory is yours to modify.

Important: When you use malloc(), you are responsible for free()ing that memory when you are done. Otherwise, you have a "memory leak" where your program gradually uses more and more memory.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
    char *names[3];
    
    // Allocate memory for each string
    names[0] = malloc(20);
    names[1] = malloc(20);
    names[2] = malloc(20);
    
    // Copy strings into allocated memory
    strcpy(names[0], "Alice");
    strcpy(names[1], "Bob");
    strcpy(names[2], "Charlie");
    
    // Now we can modify!
    names[0][0] = 'E';  // Change 'A' to 'E'
    printf("%s\n", names[0]);  // "Elice"
    
    // Dont forget to free memory!
    for (int i = 0; i < 3; i++) {
        free(names[i]);
    }
    
    return 0;
}

Dynamic memory workflow: 1) malloc(20) allocates 20 bytes and returns a pointer. 2) strcpy copies the string into that memory. 3) Now we can modify because we own this memory. 4) free() releases the memory when done. Forgetting to free causes memory leaks; freeing twice causes crashes.

2D Array vs Pointer Array Comparison

Feature 2D Array char[n][m] Pointer Array char *[n]
Memory layout Contiguous block Pointers + scattered strings
String lengths All same max length Each can be different
Memory efficiency Wastes space for short strings Uses only needed space
Modifiable (with literals) Yes No
Common use case Fixed-size data, buffers String tables, menus, args

Command-Line Arguments: argc and argv

One of the most practical uses of string arrays is handling command-line arguments. When you run a program from the terminal, you can pass additional information to it.

Example: When you type gcc myfile.c -o myprogram, the compiler receives myfile.c, -o, and myprogram as arguments. Your programs can do the same thing!

Understanding argc and argv:

  • argc (argument count) - How many strings were passed, including the program name
  • argv (argument vector) - An array of string pointers, one for each argument
  • argv[0] - Always contains the program name or path
  • argv[1] through argv[argc-1] - The actual arguments
#include <stdio.h>

// argc = argument count, argv = argument vector (array of strings)
int main(int argc, char *argv[]) {
    printf("Program name: %s\n", argv[0]);
    printf("Number of arguments: %d\n", argc - 1);
    
    for (int i = 1; i < argc; i++) {
        printf("Argument %d: %s\n", i, argv[i]);
    }
    
    return 0;
}

// Run as: ./program hello world 123
// Output:
// Program name: ./program
// Number of arguments: 3
// Argument 1: hello
// Argument 2: world
// Argument 3: 123

How to use this: Compile your program, then run it from the terminal with arguments: ./program hello world 123. The loop starts at i = 1 to skip the program name. You can use these arguments to control program behavior, specify filenames, or pass configuration options.

Remember: argv[0] is always the program name. Actual arguments start at argv[1]. The value argc includes the program name, so if you pass 3 arguments, argc will be 4.

Practice Questions: Arrays of Strings

Test your understanding of string arrays.

Question: Create an array to store 4 menu options: "New", "Open", "Save", "Exit". Print each with its number.

View Solution
const char *menu[] = {"New", "Open", "Save", "Exit"};
int count = sizeof(menu) / sizeof(menu[0]);

for (int i = 0; i < count; i++) {
    printf("%d. %s\n", i + 1, menu[i]);
}

Question: Given an array of strings, write code to count the total number of characters across all strings (not including null terminators).

View Solution
const char *words[] = {"Hello", "World", "C"};
int count = sizeof(words) / sizeof(words[0]);
int total = 0;

for (int i = 0; i < count; i++) {
    total += strlen(words[i]);
}

printf("Total characters: %d\n", total);  // 11
06

Interactive Demo

Explore how strings work in C with this interactive demonstration. Visualize the null terminator, see string functions in action, and understand memory layout.

String Memory Visualizer

Enter a string to see how it is stored in memory

String Length: 0
Memory Used: 1 byte
Null Position: Index 0
Memory Layout:
0
'\0'
0

Blue = Regular characters Red = Null terminator

String Function Simulator

See how common string functions work step by step

// Select a function and click Run
Result: -

Quick Check: Character or String?

Test your understanding of single vs double quotes

'A'
Score: 0/0

Key Takeaways

Strings Are Character Arrays

In C, strings are arrays of characters ending with a null terminator '\0' that marks the end

Null Terminator is Essential

The '\0' character tells functions where the string ends - without it, disaster awaits

Use string.h Functions

strlen, strcpy, strcat, strcmp - these standard functions handle common string operations safely

Prefer Safe Functions

Use strncpy, strncat, and fgets instead of their unsafe counterparts to prevent buffer overflows

Size = Length + 1

Always allocate one extra byte for the null terminator when sizing character arrays

Use fgets for Input

fgets is safer than scanf or gets because it limits input length and prevents buffer overflows

Knowledge Check

Quick Quiz

Test what you have learned about strings in C

1 What character marks the end of a string in C?
2 What is the minimum array size needed to store the string "Hello"?
3 Which function is used to find the length of a string?
4 Why should you avoid using gets() for string input?
5 What does strcmp(str1, str2) return if str1 and str2 are equal?
6 Which declaration creates a modifiable string?
Answer all questions to check your score