Pointers & References
What a pointer guarantees that a reference doesn't, and vice versa.
Beginner
* for you. But the differences that actually matter
for choosing between them are about what each one guarantees.
| Pointer | Reference | |
|---|---|---|
| Can be null | yes | no — always refers to something |
| Can be reassigned | yes, to point elsewhere | no — bound once, at initialization |
| Needs initialization | no (dangerous: uninitialized pointers point nowhere in particular) | yes — there's no such thing as an unbound reference |
| Use it when | you need to reassign, or express "may not point to anything" (or use nullptr) | you want a guaranteed non-null alias that never needs to be redirected |
const to the left of * makes the pointed-to data
const. const to the right of * makes the pointer itself
const. They're independent — you can have either, both, or neither.
int val = 1;
const int *ptr = &val; // data is const, pointer is not
*ptr = 2; // error: can't write through a pointer-to-const
ptr++; // fine — the pointer itself can move
val++; // fine — val isn't const, only writes through ptr are blocked
int val2 = 2;
int *const ptr2 = &val2; // pointer is const, data is not
*ptr2 = 5; // fine — writing the pointed-to int is allowed
ptr2++; // error: can't reassign a const pointer
val2++; // fine
const int *const ptr3 = &val; // both: neither the pointer nor the data can change through it
const int *ptr is "ptr is a
pointer to a const int"; int *const ptr is "ptr is a const pointer to an int."
A const pointer must be initialized where it's declared — there's no later chance to
bind it.
void* holds an address with no type information attached. That makes it
genuinely useful for "I don't care what this is yet" APIs (like memcpy), but it
comes with two hard restrictions: you can't dereference it directly — the compiler has
no idea how many bytes to read — and you can't do pointer arithmetic on it, for the
same reason. It has to be cast to a concrete type first.
int x = 42;
void *vp = &x;
// *vp; // error: can't dereference void*
// vp + 1; // error: arithmetic on void* has no defined step size
*static_cast<int*>(vp) = 100; // fine, once cast back to a concrete type
int* dangling() {
int local = 5;
return &local; // local's storage is gone the instant the function returns
} // the caller now holds a pointer to a dead stack frame
int *p = new int(5);
delete p; // the heap block is freed
// *p = 10; // undefined behavior — p still holds the old address, but it's no longer valid
p = nullptr; // the fix: null it out immediately after delete
delete doesn't prevent the original dangling
window, but it turns any later accidental use into an immediate, obvious null-dereference
crash instead of silent memory corruption — smart
pointers remove the whole class of bug by tying the delete to an object's lifetime
automatically.
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr; // arrays decay to a pointer to their first element
p[2]; // 30
*(p + 2); // 30 — identical to the line above; [] is defined in terms of this
int x = 5;
int *ptr = &x;
int **ptrToPtr = &ptr; // pointer to a pointer — holds the address of ptr itself
**ptrToPtr = 10; // x is now 10, reached through two levels of indirection
size
separate vector<int> objects on the heap, each default-constructed:
std::vector<int>* ptr = new std::vector<int>[size];
// ... use ptr[0], ptr[1], ...
delete[] ptr; // array new pairs with array delete — plain delete here is undefined behavior