Hello! Welcome to your next lesson on C programming.
In our last session, we learned how to use standard library functions like strlen, strcmp, and strcpy to work with C-strings, which we understood as null-terminated character arrays. This provided a practical foundation for string manipulation.
Today, we're going to dive deeper and investigate a subtle but critical distinction in C: the difference between initializing a string as a character array versus a pointer to a string literal. This is a classic C concept that trips up many developers, and understanding it is fundamental to grasping how C manages memory—a key part of your goal to learn what's happening "under the hood."
By the end of this lesson, you will be able to distinguish between character arrays and string literals in memory, understanding where they are stored, whether they can be modified, and why they behave differently.
Two Ways to Define a String
Let's start with two declarations that look very similar:
// Method 1: A character array initialized by a string
char arr_str[] = "hello world";
// Method 2: A pointer initialized with a string literal
char *ptr_str = "hello world";
If you were to print both using printf("%s", ...);, they would produce the exact same output. However, behind the scenes, the C compiler treats them in fundamentally different ways. This difference revolves around where the string's characters are stored in memory and whether that memory is modifiable.
Practical Differences in Action
The best way to see the difference is to watch it in action. The following video does an excellent job of demonstrating the practical consequences of these two declaration styles.
String In Char Array VS. Pointer To String Literal | C Programming Tutorial
This video provides a fantastic overview of the two ways to declare strings, demonstrating the crucial differences in modifiability, reassignment, and how the sizeof operator behaves for each.
Watch the video from 00:38 to the end (09:48). Pay close attention to: Where char s1[] is stored (stack) and that it's modifiable. Where char *s2 points to (a read-only part of memory) and why modifying it causes a crash. The differences in re-assignment (s1++ vs s2++) and using the sizeof operator. The recommendation to use const char * as a best practice.
Unpacking the Differences
As the video demonstrated, the two forms are not interchangeable. Let's summarize the key distinctions in a table.
| Feature | char arr[] = "text"; |
char *ptr = "text"; |
|---|---|---|
| Memory for "text" | A copy of the string is placed in a new array, typically on the stack for local variables. | The string literal "text" is stored in a read-only part of the program's memory. |
| Modifiability | Yes. You can change characters in the array (e.g., arr[0] = 'T';). |
No. Attempting to modify the literal (e.g., ptr[0] = 'T';) results in a crash (segmentation fault). |
| Re-assignment | No. The array name arr is a constant location; you cannot make it point somewhere else (arr = ... is a compile error). |
Yes. The pointer ptr is a variable; you can make it point to a different string (ptr = "new text";). |
sizeof(variable) |
Returns the size of the array in bytes (e.g., 5 for "text" because of the 4 letters + \0). |
Returns the size of a pointer on your system (typically 4 or 8 bytes), not the length of the string. |
The "Under the Hood" View: Program Memory Layout
You've expressed a strong interest in understanding how systems work at a low level. The reason for the behaviors above lies in how a C program's memory is organized.
When your program runs, the operating system gives it a virtual memory space that is typically divided into a few key segments. For this lesson, the two most important are:
- The Stack: This is where local variables (those declared inside functions) are stored. It's a read/write area of memory that grows and shrinks as functions are called and return. It's fast and automatically managed.
- The Data Segment: This is where global and static variables are stored. Crucially, a part of this segment is often marked as read-only, and it's where the compiler stores constants, including string literals like
"hello world".
This next reading explains this memory layout.
The video showed what happens. Now let's explore why. To understand this, we need a mental model of how a C program organizes its memory. This document explains the different memory segments.
Read the sections titled 2. The Layout of Memory and 3.4 Strings. Section 3.4 is the most important part—it directly explains that string literals are typically stored in the 'static segment' (the data segment) for optimization, whereas initializing a char[] creates a copy of the string in space allocated for the array (on the stack for local variables).
So, let's connect this theory to our examples:
-
char arr_str[] = "hello";- The compiler sees you need an array on the stack large enough for
"hello". - It allocates 6 bytes (
h, e, l, l, o, \0) on the stack. - It copies the characters of the string literal into this stack-allocated array.
- Since
arr_stris on the stack, you are free to modify its contents.
- The compiler sees you need an array on the stack large enough for
-
char *ptr_str = "hello";- The compiler places the string literal
"hello"in the read-only data segment. - It allocates a pointer
ptr_stron the stack. - It sets the value of the pointer
ptr_strto be the memory address of the first character ('h') of the string literal in the read-only segment. - When you try to write to
ptr_str[0], you are attempting to modify the read-only memory, which the operating system forbids, causing a segmentation fault.
- The compiler places the string literal
A Modern Best Practice: const Correctness
Since string literals are non-modifiable, trying to change them is a logical error. The char *ptr = "..." syntax is an old, pre-standard C feature that is unfortunately still allowed for backward compatibility. Modern C compilers will warn you about it.
The correct, safer way to declare a pointer to a string literal is by using the const keyword:
const char *ptr_str = "hello world";
This tells the compiler, "I am creating a pointer to character data that I promise not to change." Now, if you accidentally write code that tries to modify it:
ptr_str[0] = 'H'; // This will now cause a COMPILE-TIME error!
Catching this error at compile time is far better than having your program crash at runtime. It's a way of using the type system to prevent bugs, a practice common in more modern languages that you can and should apply in C.
Practical Exercise
Let's create a program to demonstrate these concepts safely.
- Create a new file named
string_memory.c. - Write the following code, which puts these ideas into practice. Read the comments carefully as they explain each step.
#include <stdio.h>
#include <string.h> // For strlen()
int main() {
// Method 1: Character array on the stack.
// The content of the literal "modifiable" is COPIED here.
char arr_str[] = "modifiable";
// Method 2: Pointer to a string literal in read-only memory.
// ptr_str holds the ADDRESS of the literal "read-only".
// We use 'const' for safety and correctness.
const char *ptr_str = "read-only";
printf("--- Initial Strings ---\n");
printf("Array string: %s\n", arr_str);
printf("Pointer string: %s\n\n", ptr_str);
// --- Let's check their sizes ---
printf("--- sizeof Operator ---\n");
printf("sizeof(arr_str): %zu bytes (size of the array: 10 chars + '\\0' = 11)\n", sizeof(arr_str));
printf("sizeof(ptr_str): %zu bytes (size of a memory address/pointer)\n\n", sizeof(ptr_str));
// Note: strlen() would give 10 for both, as it counts characters, not memory size.
// printf("strlen(arr_str): %zu\n", strlen(arr_str));
// printf("strlen(ptr_str): %zu\n", strlen(ptr_str));
// --- Let's try to modify them ---
printf("--- Modifiability Test ---\n");
// Modify the array version - this is perfectly fine.
arr_str[0] = 'M';
printf("Modified array string: %s (This works!)\n", arr_str);
// Try to modify the pointer version.
// The line below is commented out because it would cause a compile-time error
// thanks to our use of 'const'. If 'const' were removed, it would compile but
// crash the program at runtime (segmentation fault).
// ptr_str[0] = 'R';
printf("Cannot modify the pointer's literal. The line 'ptr_str[0] = 'R';' is a compile error.\n\n");
// --- Let's try to re-assign the pointer ---
printf("--- Re-assignment Test ---\n");
printf("Original ptr_str pointed to: %s\n", ptr_str);
ptr_str = "a different literal"; // This is fine, we're just changing where the pointer points.
printf("New ptr_str points to: %s\n", ptr_str);
return 0;
}
- Compile and run the program:
gcc -Wall -g -o string_memory string_memory.c && ./string_memory - Observe the output and confirm it matches the explanations in the code and this lesson.
Conclusion
Today, you've learned one of the most important low-level details about strings in C. This knowledge is crucial for avoiding common bugs and writing robust code.
Key Takeaways:
char arr[] = "text";creates a modifiable copy of the string in a local array, typically on the stack.sizeof(arr)gives the array's size.char *ptr = "text";creates a pointer that points to a read-only string literal in the data segment. Attempting to modify it causes a crash.sizeof(ptr)gives the pointer's size.- Best Practice: Always use
const char *when creating a pointer to a string literal. This allows the compiler to catch modification errors for you.
This lesson serves as a perfect bridge to our next module, where we will dive much deeper into the world of pointers and memory management. We'll formalize our understanding of memory addresses (&), dereferencing (*), and see how pointers are fundamental to almost everything we do in C, from function arguments to array manipulation.