Pointers, Arrays, and Strings
Lesson 4 of 8
4Pointers, Arrays, and Strings
Pointers are the concept that truly sets C apart: instead of dealing with values alone, you deal directly with the addresses of where they live in memory.
What the picture shows: x lives at address 0x1000, the pointer p holds that address, so *p = 7 writes straight into x's cell.
Map of this part
The concepts in order, click any one to jump to it:
Two ways into the same idea
Memory is a long street of houses, each with a number. An ordinary variable is what is inside a house; a pointer is a slip of paper with the house number on it. With the slip you can go to the house and change what is inside, or move to the house next door. An array is a row of houses side by side.
A pointer int *p holds an address; &x gives a variable's address and *p reaches the value. An array is a contiguous block whose name decays to a pointer to the first element, and p + 1 moves by the type's size. A C string is a char array ending in \0.
Before You Start
⚙️C fundamentals
You need full comfort with variables and functions from Part Four before this part.
The Core Idea: A Pointer as an Address
Figure 1, a pointer isn't the value itself; it's a variable that stores the address of where that value lives in memory.
1& and *
&x gives you the address of variable x. *ptr "dereferences" and reaches the value stored at the address ptr holds. Mechanically, the compiler turns *ptr into a single processor instruction that reads memory at that address, no extra searching or indexing involved; this directness is part of why C is fast: accessing through a pointer is as close to a raw hardware operation as the hardware itself allows.
Hold one pointer first
Before arrays and strings, a ten-line program with the whole idea: a variable, its address, and changing it through the address.
#include <stdio.h>
int main(void)
{
int x;
int *p;
x = 5;
p = &x;
printf("x = %d\n", x);
printf("*p = %d\n", *p);
*p = 9;
printf("x = %d\n", x);
return (0);
}
p = &x stored the address of x in p. *p means "the value at that address", so 5 was printed. Then *p = 9 wrote to that same address, and x changed without being touched by name. Understand these three lines and you understand pointers; the rest is application.
2An array = a pointer to its first element
An array's name arr effectively acts as the address of its first element; arr[i] is literally shorthand for *(arr + i). The important part here is that "+1" on a pointer doesn't mean "add one byte", the compiler automatically multiplies by the element type's sizeof; so arr + 1 on an int array advances a full 4 bytes (int's usual size), not one byte, because it needs to jump to the entire next element, not its middle. This is exactly what makes arr[i] work correctly no matter what type the array holds.
3Strings = a char array ending in \0
There's no independent "string" type in C, a string is just a char array ending in a special zero-value character (Null Terminator) that tells functions where to stop reading. Why this particular design? Because C doesn't store a string's length in a separate field the way some other languages do (Pascal, for example, stores the length before the content), relying purely on "keep reading until you hit zero" saves the memory bytes that would otherwise store the length, but it costs you in return: computing a string's length (what strlen does) requires walking every character one by one until it hits \0, in O(n) time, unlike a language that stores the length upfront and reads it directly in O(1).
4Struct and typedef
struct groups several values of different types under one name; typedef gives that composite type a short name for easy use later. In memory, a struct's fields are stored roughly adjacent in the order you declared them (with possibly a small gap between fields called padding, which C adds to align each field at an address that suits its type and speeds up the processor's access to it), this is fundamentally different from typedef, which doesn't change a single byte in memory; it's purely an alias the compiler understands at compile time and that then disappears entirely from the final program.
A direct example: we need to represent a point with x and y coordinates together as one unit, instead of two separate variables you manually keep in sync:
struct point_s
{
int x;
int y;
};
typedef struct point_s point_t; /* now point_t is a short name for struct point_s */
point_t p;
p.x = 3;
p.y = 7; /* access each field through the dot (.) */
Figure 2, Without typedef you'd write struct point_s p; every time, typedef just saves you repeating the word struct, and adds no new behavior to the program.
\0 when building a string manually. Any function that handles strings (like printf("%s")) keeps reading past the array's actual allocated bounds until it happens to hit a random zero byte in memory, dangerous Undefined Behavior.
How It Works, Step by Step: Swapping Two Values via Pointers
void swap(int *a, int *b)
{
int temp;
temp = *a; /* save the value a points to */
*a = *b; /* put b's value where a points */
*b = temp; /* put the originally saved value where b points */
}
int main(void)
{
int x = 5, y = 10;
swap(&x, &y); /* we pass the ADDRESSES of x and y, not their values */
printf("x=%d y=%d\n", x, y); /* x=10 y=5 */
return (0);
}
Figure 3, If we passed x and y directly instead of their addresses, their values would be copied locally inside the function and nothing in main would change, this is exactly why pointers exist: passing the ability to modify, not just a copy of a value.
Myth vs. Reality
| The myth | The reality |
|---|---|
| Arrays and pointers are always exactly the same thing. | They behave the same way in most contexts, but sizeof(arr) gives the array's full size while sizeof(ptr) gives only the pointer's own size (usually 8 bytes). |
| Pointers are an advanced concept that can be postponed. | They're the foundation of everything later in C: efficiently passing large data, building data structures, and dynamic memory management are all built on understanding pointers. |
Deep Dive: Process Memory, Pointer Arithmetic and argv
Here we dive into the idea that governs the rest of the track: the address. We see where things sit in a process’s memory, how two-dimensional arrays are stored, what a pointer to a pointer means, what argv carries, and how safe the string functions are.
1. The layout of a process’s memory
When your process runs, the system divides its memory into regions each with a purpose. Text holds the program’s instructions, read-only. Data holds initialized global and static variables, and BSS the uninitialized ones. The heap is a region growing upward for what you reserve with malloc. The stack grows downward for functions’ local variables, parameters and return addresses, creating a frame at each call and erasing it on return. String literals such as "Hello" are kept in a read-only region, which is why modifying them causes a crash.
Figure 4, The process memory layout: the stack and heap grow in opposite directions.
2. Pointer arithmetic
Adding an integer n to a pointer does not add n bytes but n × sizeof(type). If p points to an int (4 bytes) then p + 1 jumps 4 bytes to the next element. From this comes the relation between array and pointer: the expression a[i] is defined as *(a + i). An array is not a pointer (sizeof gives the size of the whole array), but it decays to a pointer to its first element when passed to a function, so the function does not know its length and you must pass it.
Figure 5, An int array: each pointer step jumps by the size of the type (4 bytes here).
3. A two-dimensional array in memory
Memory is linear, so how is a table stored? C places the rows one after another, contiguous (row-major order). So int m[2][3] is six consecutive numbers, and the element m[i][j] lies i × 3 + j elements from the start. That is why the number of columns must be known when passing a two-dimensional array to a function, because it is what the compiler uses to compute the position.
| Index | m[0][0] | m[0][1] | m[0][2] | m[1][0] | m[1][1] | m[1][2] |
|---|---|---|---|---|---|---|
| Offset | 0 | 1 | 2 | 3 | 4 | 5 |
4. A pointer to a pointer and argv
A pointer is a variable holding an address, so it has an address of its own, and another pointer can hold that address: a pointer to a pointer (int **). Its first benefit is to modify, from inside a function, the pointer itself and not what it points to, because the function receives a copy. Its second is to represent an array of strings. And this is exactly what the kernel passes to your program: argv is an array of pointers to strings (char **argv) ending with a NULL pointer, and argc is the count of its elements. argv[0] is the program’s own name, so the real arguments start at argv[1].
Figure 6, Calling ./prog hello 42: argc equals 3 and argv is an array of pointers ending with NULL.
And every argument arrives as text even if you typed a number: the value "42" is a string of two characters, and it needs converting to a number before computing. If a function does not use argc or argv, tell the compiler with (void)argc or __attribute__((unused)) so it does not warn about an unused variable.
5. Strings: what the terminating zero guarantees
A string is a character array ending with the null character \0, and every library function relies on it to find the end. That is why strlen counts up to the zero without counting it, and reserving an array needs an extra character for it. The problem is with functions that copy without knowing the destination size: strcpy and strcat write until they find the zero in the source even if they overrun the destination, and that is a buffer overflow. The bounded strncpy does not add a final zero when it reaches the limit, so it needs care. The rule: always know the destination size and make sure the text ends with a zero.
Believing that sizeof(s) gives the length of a string that reached the function as a pointer. Inside the function it gives the pointer’s size (8 bytes), not the number of characters; use strlen or pass the length.
Try it yourself
What does *ptr do?
& gives a variable's address and * dereferences it to reach the value.
In C, arr[i] is equivalent to:
arr[i] is literally shorthand for *(arr + i). And adding 1 to a pointer advances by the size of the type, not one byte.
What makes a char array a “string” in C?
There is no separate text type; a string is a char array ending in \0.
Quick Review Flashcards
Check Your Understanding
If you forgot & when calling swap(x, y) instead of swap(&x, &y), what happens?
Likely a compile error (since the function expects int* pointers, not int values), or in the worst case undefined behavior, and importantly: nothing would actually swap because the function would operate on local copies, not the original x and y.
Does typedef actually create a new type, or just an alternate name?
Just an alternate name (alias), it doesn't change anything about the data's actual structure, it only makes code easier to read and shortens writing out a long struct every time.
What is the offset of m[1][2] in int m[2][3]?
1×3+2 = 5, the last element.
Why does ./prog 42 break if you treat argv[1] as a number directly?
Because it is the text "42", not a number, and must be converted first.
Resources for this part
Sorted by kind: start with the official docs when you want the precise reference, or with an article or video when you want the explanation first.
Resources for this lesson
Grouped by type. Start with the official docs for precision, or an article or video for the explanation.
Official docs1
Articles3
Done with this lesson?
Sign in to save your progress.