pointer vs. reference

A reference can be thought of as a constant pointer with automatic dereferencing — the compiler inserts the * for you. But the differences that actually matter for choosing between them are about what each one guarantees.
PointerReference
Can be nullyesno — always refers to something
Can be reassignedyes, to point elsewhereno — bound once, at initialization
Needs initializationno (dangerous: uninitialized pointers point nowhere in particular)yes — there's no such thing as an unbound reference
Use it whenyou 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 and pointers — which side of the * matters

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
            
Read it right-to-left from the variable name: 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 pointers

A 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
            

dangling pointers

A pointer doesn't know when the thing it points to has been destroyed — using it after that point is undefined behavior, not a guaranteed crash, which is exactly what makes it dangerous. Two common ways to get here:

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
            
Nulling a pointer right after 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.

pointer arithmetic and pointer-to-pointer


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
            
Allocating an array of pointers looks like this — note this allocates 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
            

heap vs. stack ownership, worked example

A class can hold two pointers that point to the same object (stack) or to two independently-allocated objects that happen to hold equal values (heap) — only the first is a "shallow copy" in the sense that changing one changes the other: TopNotchNote/cpp/pointers_shallow_vs_independent.cpp

where to go from here

Smart Pointers — automating the delete you'd otherwise have to remember.
Move Semantics — transferring ownership of heap resources without copying.
Casting — the four C++-style casts, including when a void* needs one.

reference

cppreference — pointer declaration
cppreference — reference declaration