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.
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."
'\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.
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.
// 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
// 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
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.
'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 stringb: Has null terminator - valid stringc: 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 bytesstrlen(str)returns 2 - the number of characters before '\0'
Remember: sizeof gives array size, strlen gives string length!
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.
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:
- You write text in double quotes:
"Hello, World!" - The compiler counts the characters (13 letters + 1 null = 14 bytes needed)
- C creates an array of exactly that size
- Each character is copied into the array
- 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.
'\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].
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:
// 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
// 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.
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:
- Cannot assign to arrays after declaration. Use
strcpy(). - "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!
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):
// 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 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 <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!
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.
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"andsrc = " World" - strcat(dest, src) finds the
'\0'in dest, overwrites it with ' ', and continues copying - After:
dest = "Hello World\0"
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.
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".
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 strstrrchr(str, 'x')- Finds the LAST occurrence of character 'x' in strstrstr(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
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.
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 optionsputs(str)- Simplest, automatically adds a newline at the endputchar(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.
How scanf("%s") Works
Skip Whitespace
Ignores any leading spaces, tabs, or newlines before the actual input
Read Characters
Reads characters one by one and stores them in your buffer
Stop at Space
Stops immediately when it encounters a space, tab, or newline
Add Null
Automatically adds '\\0' at the end to make it a valid string
User types: John 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.
char buffer[10];
scanf("%s", buffer); // No size limit!
// User can overflow buffer
char buffer[10];
scanf("%9s", buffer); // Max 9 chars + '\0'
// Cannot overflow 10-byte buffer
gets() - NEVER USE THIS!
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:
- You tell it: "Read into this buffer, but NEVER more than N characters"
- It reads characters until: newline, EOF, or reaching the limit (whichever comes first)
- It ALWAYS adds a null terminator (guaranteed safe string)
- 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.
fgets()
Syntax: char *fgets(char *str, int n, FILE *stream);
Parameters:
str: Buffer to store the stringn: Maximum characters to read (including null terminator)stream: Input stream (usestdinfor 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.
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:
- No size limit on scanf - if user enters more than 7 characters, buffer overflow occurs
- Password will be visible on screen (security issue, though not related to strings)
Fix: Use scanf("%7s", password) or better, use fgets()
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.
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' | 0 | 0 | 0 | 0 | 0 | 0 |
| names[1] | 'S' | 'a' | 'm' | '\0' | 0 | 0 | 0 | 0 | 0 | 0 |
| 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.
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.
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.
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 nameargv(argument vector) - An array of string pointers, one for each argumentargv[0]- Always contains the program name or pathargv[1]throughargv[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.
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
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
Memory Layout:
Blue = Regular characters Red = Null terminator
String Function Simulator
See how common string functions work step by step
// Select a function and click Run
Quick Check: Character or String?
Test your understanding of single vs double quotes
'A'
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