Pointers in C/C++ - what a pointer is, how pointer syntax works, and why programmers care

One of the hardest things for new programmers to learn is pointers. Whether it is pointers by themselves, pointers that point to arrays, or pointers that point to pointers, something about this concept just drives people a little crazy. If you are feeling that right now, you are in good company. I was that person when I learned C/C++ back in the day. I stared at asterisks like they were tiny snowflakes of doom and wondered why my program kept exploding with a segmentation fault.
In this article, I am going to show you exactly what a pointer is so you can fully understand how they work. You will learn how to read pointer syntax at a glance without sweating, and you will finally see why everyone cares so much about pointers and what they are used for in the real world. Grab a coffee, take a breath, and let us turn this scary topic into something you will be proud to use.
Prefer watching instead of reading? You can watch the full walkthrough below, or keep scrolling to read the complete article.
What is a pointer in C/C++
Here is the punchline up front. A pointer is just a value that happens to be an address. That is it. It is a number whose meaning is special because we agree that it represents a location in memory. When you set the value of one variable equal to the address of another variable, you have created a pointer. No secret handshake. No magic. Just numbers and meaning.
Before pointers, let us talk about memory
To understand pointers, we need to understand the idea of memory like the computer sees it. Picture a long street of mailboxes. Every mailbox has a number painted on it - that is the address. Inside each mailbox is a card with some data on it - that is the value. The address tells you where to go. The value tells you what lives there.
If we talk in C/C++ terms, we can label an address with a nice hex number. Imagine a box labeled 0x1000. If we put the number 4 inside that box, that means the value at address 0x1000 is 4. If you say int x = 4;, the compiler finds a spot for x on the stack, gives it an address like 0x1000, and puts the value 4 there. That is it. Nothing spooky yet.
Creating the first pointer
Now let us say we do something sneaky. At address 0x1004, we store the number 0x1000. Did you catch that? We just put an address inside another slot of memory. That value is not a normal number now. We will treat it as an address that points at the location of x. Congratulations. That is a pointer.
In code, the way we say that looks like this:
int x = 4; // x lives somewhere,
// say 0x1000
int *px = &x; // px lives somewhere else, say 0x1004, and holds 0x1000 //
The star next to int says px is not a regular int. It is a pointer to an int. The ampersand says give me the address of x. So the sentence you should hear in your head is: int pointer px is set to the address of x. When you frame it like that, the glyphs are just punctuation for this sentence.
Reading pointer syntax out loud
New programmers often trip over the combination of stars and ampersands and arrows and more stars. So here is my trick. Read the line like you would explain it to a human.
- int x = 4; - an integer named x is set to 4.
- int *px = &x; - a pointer to int named px is set to the address of x.
Saying it out loud locks in the meaning. The star next to the type modifies the type. It says we are not storing an int here. We are storing a pointer to an int here. The ampersand in front of a variable gives the address of that variable.
Dereferencing pointers - using the thing pointed to by
Having a pointer is not very useful until you use it to grab or change the value it points at. That is where dereferencing comes in. When you put a star in front of a pointer variable by itself, you are saying the thing pointed to by this pointer. That is the move that jumps to the address and reads or writes the value at that address.
int x = 4;
int *px = &x;
// Read through the pointer
int y = *px; // y gets 4
// Write through the pointer
*px = 10; // now x is 10
When there is a type nearby, the star modifies the type. When the star stands alone next to a pointer variable, it dereferences it. Reading voice in your head helps: int y is set to the thing pointed to by px. Or the thing pointed to by px is set to 10. That little mental script keeps the weirdness under control.
Why pointers matter - pass by reference and clean architecture in C
A lot of folks ask why we even bother with pointers. The syntax looks cryptic. Programs crash when you mess them up. Why torture yourself? The answer is very practical. In C, you need pointers to write clean, modular code that does not copy giant chunks of data every time you call a function. Without pointers, you end up with bloated code and performance problems.
Passing structs by reference
Imagine you have a struct Person with a name and an age. You want a function that updates the age. The person you want to edit was created in a different scope. If you pass the whole struct by value, you copy it. The function edits the copy, and the original stays the same. That is not what you want. Also, copying large structs all over the place is wasteful.
typedef struct {
char name[32];
int age;
} Person;
void update_age(Person *p, int new_age) {
p->age = new_age; // use arrow to access fields through a pointer
}
int main(void) {
Person alice = {"Alice", 29};
update_age(&alice, 30); // pass address of alice
// alice.age is now 30
}
The arrow operator is just a convenience for dereferencing then using dot. p->age is the same as (*p).age. Some beginners think the arrow is magic. It is not. It is sugar for the thing pointed to by p, then grab the age field. This style keeps functions focused on their action, like update_age, while avoiding surprise copies.
Static vs dynamic memory - why heap pointers are unavoidable
Another reason pointers are everywhere in C is dynamic memory. Static allocation puts variables on the stack. That memory belongs to the current function and goes away when the function returns. Dynamic allocation pulls memory from the heap. The heap gives you memory whose lifetime can outlive the current function. The heap hands memory to you as a pointer.
char *buf = malloc(100); // ask heap for 100 bytes
if (!buf) {
// handle out-of-memory
}
// use buf ...
free(buf); // give it back when done
When you call malloc, you do not get a char array in your hands. You get a char pointer that tells you where that array lives. The array might be 100 bytes or 10,000 bytes. The size can come from the user. It can grow if you call realloc. Stack variables are fixed size and known at compile time. Heap allocations are flexible and chosen while the program runs. That is why the pointer is the handle you carry around to access that memory later.
If you have heard about sbrk, that is a very low level way to grow the heap on some systems. Most code should not touch it directly. Malloc handles the gritty details for you. But either way, the interface boils down to pointers. You ask for memory and you get back an address that you must manage.
Pointers to pointers - yes, the inception layer
Early on I said pointers that point to pointers make people sweat. Let us demystify that. A pointer to a pointer is just what it sounds like. Instead of holding the address of an int, you hold the address of a pointer that holds the address of an int. Say that slowly and it clicks. It is two hops instead of one.
int x = 4;
int *px = &x; // pointer to int
int **ppx = &px; // pointer to pointer to int
// Read x through two hops
int y = **ppx; // y is 4
// Update x through two hops
**ppx = 20; // x is now 20
Why would you want this? A classic use case is a function that needs to update a caller's pointer. For example, suppose you have a function that may allocate memory and hand the address back to the caller. If you pass char *buf, the function can write into the memory buf already points at, but it cannot change buf itself for the caller. If you pass char **buf, now the function can set *buf to a new allocation and the caller sees it.
bool make_buffer(char **out, size_t n) {
char *tmp = malloc(n);
if (!tmp) return false;
*out = tmp; // write back to caller
return true;
}
int main(void) {
char *buf = NULL;
if (make_buffer(&buf, 256)) {
// buf now points to 256 bytes
free(buf);
}
}
Once you are comfortable saying the thing pointed to by in your head, double stars are not scary. You just repeat it twice. The thing pointed to by the thing pointed to by ppx. Boom.
Arrays and pointers - why arr decays to a pointer
Arrays and pointers hang out together a lot in C. When you pass an array to a function, the array name decays to a pointer to its first element. That is a fancy way of saying the function sees an address, not the whole array object. This explains why you also pass the length as a separate argument. The pointer knows where the first element lives but not how many there are.
void fill(int *arr, size_t n, int value) {
for (size_t i = 0; i < n; i++) {
arr[i] = value; // same as *(arr + i) = value
}
}
int main(void) {
int scores[5] = {0};
fill(scores, 5, 7); // scores decays to &scores[0]
}
This is also where pointer arithmetic shows up. arr + 1 moves by one element, not one byte. For ints, that is usually 4 bytes. For doubles, 8 bytes. The type of the pointer controls how far it steps. That is why the type part of a pointer really matters. It is not just a label. It changes how math works on the pointer.
Common pointer mistakes that crash programs
You are going to make mistakes. That is part of learning. Here are the hits that get beginners over and over, plus how to dodge them.
- Using an uninitialized pointer. If you have int *p; and you never set p, it holds garbage. Dereferencing it is a coin flip with a snake on both sides. Always initialize pointers to NULL or to a real address.
- Returning the address of a local variable. Locals live on the stack and go away when the function returns. If you return &localVar, that pointer points into memory that is no longer yours. The fix is to allocate on the heap or have the caller pass a pointer to a destination.
- Forgetting to free or double free. If you never free, memory leaks. If you free twice, you corrupt the heap. Make a clear ownership rule for who frees what.
- Off by one with arrays. Writing to arr[n] when the valid indices are 0 to n-1 will smash the next thing in memory. The compiler will not save you from this. Be careful with loop bounds.
- Mixing up const pointer and pointer to const. const int *p means you cannot change the int through p. int *const p means p cannot point somewhere else, but you can change the int. const int *const p means both are fixed.
How big is a pointer - and why the size matters
On a 32 bit system, pointers are typically 4 bytes. On a 64 bit system, pointers are typically 8 bytes. That size is the size of an address. You do not pick that. The platform decides. This matters because if you try to stuff a pointer into an int on a 64 bit system, you lose pieces and crash at random times. Use the correct types for addresses, like uintptr_t from stdint.h if you need an integer type that can hold a pointer value.
#include <stdint.h>
uintptr_t bits = (uintptr_t)ptr; // store pointer as an integer safely
void *ptr2 = (void *)bits; // cast back if you must
The other reason size matters is alignment. Many platforms expect certain types to be aligned at certain addresses, like a 4 byte boundary for int. Pointer arithmetic respects that by moving in element sized steps. When you use malloc, the memory it returns is aligned enough for any built in type, so you do not need to fix it by hand.
Pointer reading technique - verbalize every symbol
When code gets hairy, slow down and verbalize it. Here is a messy line taken apart.
int **const ppx = &px;
- Start with the base: int.
- There are two stars next to the name, so this is a pointer to pointer to int.
- const is to the right of the stars here, so the pointer itself is const. ppx cannot be made to point elsewhere.
- It is set to the address of px.
Say it fully: const pointer to pointer to int named ppx is set to the address of px. This habit works for function pointer types too, which look wild but are just punctuation around the same idea of addresses to things.
By value vs by reference - what actually happens with memory
When you pass a variable by value, the function gets a copy of the value. Changing the function parameter does not change the original. When you pass the address of the variable instead, the function gets a pointer. The function can dereference that pointer and change the original. This is how you write functions that return multiple results or modify big structures without copying tons of bytes.
void set_to_zero(int *p) {
*p = 0; // change the original int
}
int main(void) {
int x = 5;
set_to_zero(&x);
// x is now 0
}
That is the entire trick. Address in. Dereference inside. Change the original. No hidden pass by reference like in other languages. In C, you do it yourself with pointers, and you can see exactly what is going on.
Pointer arithmetic - walking memory like a pro
Pointer arithmetic sounds scary but it is just indexes in disguise. If p is an int *, then p + 1 points to the next int. If p is a char *, p + 1 points to the next char. The compiler multiplies by the size of the thing automatically. That is why incrementing a void * is not allowed in standard C. A void pointer has no size, so the compiler does not know how far to move.
int arr[3] = {10, 20, 30};
int *p = arr; // same as &arr[0]
int a = *p; // 10
int b = *(p + 1); // 20
int c = *(p + 2); // 30
You can subtract pointers too, but only pointers that point inside the same array. That gives you how many elements apart they are. Do not do math on pointers from unrelated allocations. That is like subtracting street addresses from two different towns and pretending the distance means something.
Practical mini exercises you can try right now
- Write a function that swaps two integers using pointers. Hint: swap(int *a, int *b), then use a temporary and dereference.
- Allocate a dynamic array of 100 ints with malloc. Fill it with indexes. Print them. Free it. Then change the code to read the size from user input. Watch the pointer not change, only the allocation size does.
- Build a struct with a dynamic char * for a name. Write init, set_name, and destroy functions that manage memory correctly. Pass pointers to these functions and use the arrow operator.
- Implement a function that appends to a dynamic string using char ** so the function can grow the buffer and update the caller's pointer.
- Use a pointer to const to write a function that prints an array without modifying it. Then switch to const pointer and see how your ability to reassign the pointer changes.
Reading error messages and debugging pointer bugs
When you see segmentation fault, it usually means you dereferenced an invalid pointer. Maybe it was NULL. Maybe it was uninitialized. Maybe you went out of bounds. Add printf lines or use a debugger to inspect pointer values before dereferencing. If a pointer shows up as 0x0, that is NULL. If you see something like 0x5 or 0x7, that is probably garbage or a bug where you casted wrong.
Tools help. On Linux and macOS, run under AddressSanitizer by compiling with -fsanitize=address -g. It will tell you when you go out of bounds or use freed memory, and it will point to the exact line. On Windows, there are similar tools in Visual Studio. Do not try to be a hero by guessing. Let tools catch memory mistakes early.
Putting it all together - mental model you can trust
- Memory is boxes. Every box has an address and a value.
- A variable is a named box. int x = 4 puts 4 in that box.
- A pointer is a value that is an address. int *px = &x puts x's address into px.
- Dereference uses the star next to the pointer variable to jump to the box and use the value.
- Functions that need to edit something outside their scope take a pointer to it.
- Dynamic memory gives you a pointer to a region on the heap whose lifetime you control.
- Pointers to pointers let you change a caller's pointer or handle arrays of strings like char **argv.
- Arrays decay to pointers. Always carry sizes along with them.
That is the map. When something looks confusing, slow down, read the code like a sentence, and match it to the map. If you can say in plain language what a line means, you can code it without fear.
Quick FAQ - short answers to common pointer questions
Is a pointer the same thing as a reference
In C, there is no built in reference type like in C++. When people say pass by reference in C, they mean pass the address of something with a pointer. You dereference inside the function to change the original.
Can a pointer point to anything
A pointer type tells the compiler what you claim it points to. You can cast between pointer types, but you should not invent types that do not match the actual object in memory. Void * is a generic pointer you can convert to and from safely, but you must cast back to the correct type before dereferencing.
Why does p->field work but p.field does not
The dot operator works on an object, not a pointer. The arrow operator works on a pointer to an object. p->field is shorthand for (*p).field.
What about null pointers
A null pointer is a pointer that points to no valid object. It compares equal to NULL. Dereferencing it is invalid. Initialize pointers to NULL if you do not have a target yet, check before dereferencing, and treat NULL as a signal that there is nothing to use.
If you are having a hard time with C, do not get discouraged. Pointers take a minute to click. One day you will write int *px = &x; without even blinking, then you will pass pointers to functions, allocate memory on the heap, and update things through double pointers like it is your second language. That is when you know you have leveled up into a low level wizard.
If this walk through helped, share it with a friend who is fighting with stars and ampersands right now. Drop a comment with the part that finally clicked. And if you want more deep dives like this, subscribe to get the next one. See you next week. Take care.