If there is one topic that separates C from many other beginner-friendly languages, it is pointers. Ask any experienced programmer what makes C both powerful and occasionally intimidating, and pointers are almost always part of the answer. Rather than hiding memory addresses away behind convenient abstractions, C hands them directly to the programmer, allowing extremely precise control over how data is stored, accessed, and manipulated.
This directness is exactly why pointers have a reputation for being difficult. However, once you understand what a memory address actually represents and how a pointer variable stores that address, the concept becomes far less mysterious. Pointers are not some exotic feature bolted onto the language; they are a natural consequence of how variables are stored in memory in the first place.
In this tutorial, you will learn what a pointer is, how to declare and use pointer variables, how the address-of and dereference operators work together, how pointer arithmetic behaves, how pointers relate closely to arrays, and how pointers are used when passing data to functions for modification.
A pointer is a variable that stores the memory address of another variable, rather than storing an actual data value directly. Every variable in a running program occupies a specific location in memory, and a pointer simply keeps track of where that location is.
#include <stdio.h>
int main() {
int subscribers = 25000;
int *subscriberPointer = &subscribers;
printf("Value: %d\n", subscribers);
printf("Address stored in pointer: %p", (void *) subscriberPointer);
return 0;
}
Value: 25000 Address stored in pointer: 0x7ffee2a1c9ac
The exact address displayed will vary each time the program runs, since memory locations are assigned dynamically by the operating system, but the underlying idea remains the same: the pointer variable is storing a location in memory rather than the number 25000 itself.
The address-of operator, represented by an ampersand symbol, retrieves the memory address of a variable rather than its stored value. This operator is what allows a pointer to be initialized with the location of another variable in the first place.
#include <stdio.h>
int main() {
int totalVideos = 150;
printf("Address of totalVideos: %p", (void *) &totalVideos);
return 0;
}
Address of totalVideos: 0x7ffd3c9e2a4c
A pointer variable is declared by placing an asterisk before its name, along with the data type of the value it is intended to point to. This data type is important, since it tells the compiler how many bytes to read from that memory location when accessing the value.
dataType *pointerName;
#include <stdio.h>
int main() {
int views = 5000;
int *viewsPointer;
viewsPointer = &views;
printf("Pointer holds address of views: %p", (void *) viewsPointer);
return 0;
}
Pointer holds address of views: 0x7ffc8f3e6a20
While the address-of operator retrieves a variable's location, the dereference operator does the opposite: given a pointer, it retrieves the actual value stored at the address the pointer holds. This operator is also represented using an asterisk, though its meaning depends on the context in which it appears.
#include <stdio.h>
int main() {
int subscribers = 25000;
int *subscriberPointer = &subscribers;
printf("Value accessed through pointer: %d", *subscriberPointer);
return 0;
}
Value accessed through pointer: 25000
Here, placing an asterisk before the pointer's name follows it back to the memory address it holds and retrieves the actual value stored there, which in this case matches the original value of subscribers exactly.
One of the most practical uses of pointers is modifying a variable's value indirectly, through a pointer that references it, rather than accessing the variable by its own name directly.
#include <stdio.h>
int main() {
int uploadCount = 10;
int *countPointer = &uploadCount;
*countPointer = 15;
printf("Updated upload count: %d", uploadCount);
return 0;
}
Updated upload count: 15
Even though the value was changed using the pointer rather than the original variable name, the change is reflected in uploadCount itself, since both the pointer and the original variable refer to exactly the same location in memory.
Unlike arithmetic performed on ordinary numeric variables, arithmetic performed on pointers takes the size of the data type into account, moving the pointer forward or backward by an amount proportional to that size, rather than simply by a single byte.
| Operation | Effect |
|---|---|
| pointer + 1 | Moves the pointer forward by the size of one element of its data type. |
| pointer - 1 | Moves the pointer backward by the size of one element of its data type. |
#include <stdio.h>
int main() {
int scores[3] = {85, 90, 78};
int *scorePointer = scores;
printf("First score: %d\n", *scorePointer);
scorePointer++;
printf("Second score: %d", *scorePointer);
return 0;
}
First score: 85 Second score: 90
Incrementing the pointer here moves it forward by the size of one integer, automatically landing it on the next element of the array, rather than moving forward by just a single byte.
Pointers and arrays are closely connected in C. In fact, the name of an array behaves very similarly to a pointer to its first element in most expressions, which is why array elements can be accessed using either standard indexing or pointer-based notation.
#include <stdio.h>
int main() {
int views[4] = {1500, 1800, 2100, 1950};
int *viewsPointer = views;
int i;
for (i = 0; i < 4; i++) {
printf("Day %d views: %d\n", i + 1, *(viewsPointer + i));
}
return 0;
}
Day 1 views: 1500 Day 2 views: 1800 Day 3 views: 2100 Day 4 views: 1950
Here, adding an index to the pointer and dereferencing the result produces exactly the same behavior as accessing that index directly using standard array notation, demonstrating just how closely arrays and pointers are related in C.
As covered briefly in the earlier tutorial on functions, pointers allow a function to modify a variable that exists outside its own scope, since the function receives the variable's memory address rather than a separate copy of its value.
#include <stdio.h>
void doubleViews(int *views) {
*views = *views * 2;
}
int main() {
int totalViews = 2000;
doubleViews(&totalViews);
printf("Doubled views: %d", totalViews);
return 0;
}
Doubled views: 4000
Without using a pointer here, the function would only be able to work with a copy of totalViews, meaning any changes made inside the function would have no effect once the function finished executing.
A null pointer is a pointer that has been deliberately set to point to nothing, often used to indicate that a pointer is not currently referencing any valid memory location. Checking for a null pointer before using it is an important habit that helps prevent a range of common bugs.
#include <stdio.h>
int main() {
int *dataPointer = NULL;
if (dataPointer == NULL) {
printf("Pointer is not currently assigned to any address.");
}
return 0;
}
Pointer is not currently assigned to any address.
C also allows a pointer to store the address of another pointer, creating what is known as a pointer to a pointer. While less common in everyday beginner programs, this concept becomes important in more advanced scenarios involving dynamic memory management and multi-level data structures.
#include <stdio.h>
int main() {
int subscribers = 25000;
int *pointerOne = &subscribers;
int **pointerTwo = &pointerOne;
printf("Value accessed through double pointer: %d", **pointerTwo);
return 0;
}
Value accessed through double pointer: 25000
Here, pointerTwo stores the address of pointerOne, and dereferencing it twice ultimately retrieves the original value stored in subscribers.
| Mistake | Correct Practice |
|---|---|
| Dereferencing a pointer that has not been initialized. | Always assign a valid address to a pointer, or set it to NULL, before dereferencing it. |
| Confusing the address-of operator with the dereference operator. | Remember that the address-of operator retrieves an address, while the dereference operator retrieves a value from that address. |
| Performing pointer arithmetic without considering the underlying data type's size. | Remember that pointer arithmetic moves by the size of the data type, not by a single byte. |
| Losing track of which memory a pointer is currently referencing after reassignment. | Carefully track pointer reassignments, especially within loops involving pointer arithmetic. |
Pointers give C programmers direct, precise control over memory, a capability that sets the language apart from many others that hide these details behind higher-level abstractions. By understanding how the address-of and dereference operators work together, how pointer arithmetic behaves, and how closely pointers relate to arrays, you gain access to one of the most powerful tools available in the language.
In this tutorial, you learned what a pointer actually represents, how to declare and use pointer variables, how to modify values indirectly through pointers, how pointers interact with arrays and functions, and how null pointers and pointers to pointers extend these ideas further. With pointers now part of your toolkit, you are ready to explore structures and unions, which use similar memory concepts to group different types of related data together.