Core CS Concept

Pointers for CS Students

Memory addressing explained through the lens of CS50, CS61C, and CMU 15-213 work. Dereference, address-of, pointer arithmetic, NULL and dangling pointers, smart pointers in modern C++, with real gdb sessions and Valgrind output for double-free bugs. Verified CS graduates with Memory Systems specialization, starting at $20 per task.

Pointers concept visualization showing memory addresses and indirection in c and c++
4 Languages covered
10 FAQ answers
2 Related subjects
3 Code examples

What it means

A working definition of Pointers

A pointer is a variable that stores a memory address. Dereferencing the pointer accesses the value at that address. C and C++ programs use pointers for pass-by-reference, dynamic allocation, and linked data structures.

C and C++ expose pointers explicitly with & and *; Java and Python wrap the same machinery as implicit references with null and None as the empty sentinel. Rust adds a borrow checker that statically prevents the dangling-pointer and double-free bugs that haunt C.

Primary example

address-of and dereference

address-of and dereference

          
          #include <stdio.h>
        
          
           
        
          
          int main(void) {
        
          
              int x = 42;
        
          
              int *p = &x;        // p stores the address of x
        
          
              printf("x = %d\n", x);       // 42
        
          
              printf("p = %p\n", (void*)p); // 0x7ffeefbff5ac (example)
        
          
              printf("*p = %d\n", *p);      // 42 (dereference)
        
          
              *p = 100;                    // modify x through p
        
          
              printf("x = %d\n", x);       // 100
        
          
              return 0;
        
          
          }
        

Three pointer patterns

Common pointers patterns

Address-of and dereference

The & operator takes a variable and returns its address; the * operator takes a pointer and returns the value at that address. They are inverses: *(&x) is x, and &(*p) is p. Reading int *p inside-out: "p is a pointer such that *p is an int".

Pointer arithmetic

Adding 1 to a pointer advances it by sizeof(pointed-to type), not by 1 byte. For int *p, p + 1 advances by 4 bytes; for char *p, by 1 byte. This is why arr[i] is identical to *(arr + i): the compiler scales i by the element size before dereferencing.

Smart pointers (C++)

unique_ptr owns the pointee exclusively, with automatic delete at scope exit. shared_ptr uses reference counting for joint ownership. weak_ptr breaks shared_ptr cycles. Default to unique_ptr; raw new is a code smell in modern C++.

Wrong way vs right way

Fix this pointers bug

Unchecked malloc, leaked pointer C
#include <stdlib.h>
#include <stdio.h>

int main(void) {
    int *p = malloc(sizeof(int));
    // p could be NULL here under memory pressure
    *p = 42;             // segfault if malloc failed
    printf("%d\n", *p);
    free(p);
    // p still holds the freed address; later code
    // can dereference it and corrupt the heap
    return 0;
}
Checked malloc, nullified after free C
#include <stdlib.h>
#include <stdio.h>

int main(void) {
    int *p = malloc(sizeof(int));
    if (p == NULL) {           // ALWAYS check malloc return
        fprintf(stderr, "out of memory\n");
        return 1;
    }
    *p = 42;                   // safe now
    printf("%d\n", *p);
    free(p);
    p = NULL;                  // prevent dangling pointer
    return 0;
}
malloc can return NULL when allocation fails. Dereferencing the unchecked pointer segfaults under load. The checked version reports the failure and exits cleanly; setting p = NULL after free prevents accidental double-free further down the function.

Cross-language

Same concept across languages

C++: unique_ptr and shared_ptr
#include <memory>
#include <iostream>

struct Node {
    int value;
    std::unique_ptr<Node> next;
    Node(int v) : value(v), next(nullptr) {}
};

int main() {
    // unique_ptr: exclusive ownership, no leak
    auto head = std::make_unique<Node>(1);
    head->next = std::make_unique<Node>(2);
    head->next->next = std::make_unique<Node>(3);
    // No need to delete; unique_ptr destructors
    // cascade and free all 3 nodes when head goes out of scope.

    // shared_ptr: reference-counted ownership
    auto sp1 = std::make_shared<int>(42);
    auto sp2 = sp1;  // ref count = 2
    std::cout << sp1.use_count() << std::endl;  // 2
    return 0;
}

FAQ

Pointers FAQ

What is the difference between & and * in C?
& takes the address of a variable; * dereferences a pointer to access the value at that address. int x = 42; int *p = &x; takes the address of x and stores it in p. *p reads the value at that address (42). They are inverses: *(&x) == x and &(*p) == p for any valid x and p. In declarations, int *p means "p is a pointer to int". In expressions, *p means "the int at the address stored in p" and &x means "the address of x".
Why does my program segfault when I dereference a pointer?
Three common causes. First: dereferencing NULL. Always check the return value of malloc, fopen, strchr before dereferencing. Second: dereferencing a dangling pointer (pointer to freed or out-of-scope memory). Set pointers to NULL after free and never return pointers to local variables. Third: pointer arithmetic past the end of an array (buffer overflow). Check the array bounds before subscripting. Run under gdb to get the exact line of the dereference; run under Valgrind or with AddressSanitizer (-fsanitize=address) to find the source of the bad pointer.
What does pointer arithmetic actually do?
Pointer arithmetic moves a pointer by units of the pointed-to type, not by bytes. For int *p (4-byte int), p + 1 advances by 4 bytes. For char *p, p + 1 advances by 1 byte. For struct foo *p with sizeof(struct foo) = 24, p + 1 advances by 24 bytes. This is why arr[i] is equivalent to *(arr + i): the compiler multiplies i by the element size before adding to the base address. Pointer arithmetic is bounded by the original allocation; going past the end is undefined behavior.
How do I avoid memory leaks and dangling pointers?
In C: every malloc must have a matching free, and free must happen exactly once. Set pointers to NULL after free to prevent double-free. Use Valgrind (valgrind --leak-check=full ./prog) to catch leaks and AddressSanitizer (gcc -fsanitize=address) to catch use-after-free. In C++: prefer std::unique_ptr and std::shared_ptr over raw new and delete. unique_ptr automatically deletes the pointee when going out of scope; shared_ptr uses reference counting. Avoid raw new in modern C++; use std::make_unique and std::make_shared instead.
What is the difference between unique_ptr and shared_ptr?
unique_ptr is exclusive ownership: only one unique_ptr at a time owns the pointee, and copying is a compile error. Move semantics transfer ownership. Zero runtime overhead compared to raw new and delete. shared_ptr is reference-counted joint ownership: multiple shared_ptrs can own the same pointee, and the pointee is destroyed when the last shared_ptr is destroyed. Costs one heap allocation for the control block plus atomic increment and decrement on every copy. Default to unique_ptr unless ownership is genuinely shared; use shared_ptr for graph nodes referenced by multiple edges or shared cache entries.
How do I read a complex pointer declaration like int (*p)[10]?
Read inside-out from the variable name following operator precedence. int (*p)[10]: p is a pointer (because of *), to an array of 10 (because of [10]) of int. Compare with int *p[10]: p is an array of 10 pointers to int (because [] binds tighter than *). Compare with int (*f)(int): f is a pointer to a function taking int and returning int. Use cdecl (https://cdecl.org) to translate complex declarations. The typedef trick: typedef int (*IntArrayPtr)[10]; then IntArrayPtr p; reads cleanly. Use typedefs aggressively for function pointer types and nested array types.
How does Java references differ from C pointers?
Java references behave like restricted pointers. Object o = new Object(); o is a reference to a heap object. Object o2 = o; copies the reference, both pointing to the same object. No address-of (&), no dereference (*), no pointer arithmetic, no manual free. Garbage collection reclaims unreferenced objects automatically. The null literal is the equivalent of NULL; dereferencing null throws NullPointerException instead of segfaulting. The == operator compares references; .equals compares values. Java references cannot be cast to integers, cannot point to elements within a struct, cannot do anything that requires arbitrary memory access. The tradeoff: simpler reasoning, no manual lifetime management, GC pauses instead of explicit free.
Can you debug a double-free or use-after-free with Valgrind?
Yes. valgrind --leak-check=full --show-leak-kinds=all ./prog produces output identifying invalid frees, use-after-free, and memory leaks. Example double-free output: Invalid free() / delete / delete[] / realloc() at 0x4848B3F: free, Address 0x4a91040 is 0 bytes inside a block of size 4 free'd at the previous free site, original allocated at malloc. Read the output backwards: the second free, the first free, the allocation. The path between the 3 sites is the bug. Standard tool for CS50 image manipulation psets, CS61C linked-list project, CMU 15-213 malloc lab. Our tutors include the Valgrind report and the 3-line fix in every C and C++ submission.
When should I use a pointer vs a reference in C++?
Use a reference (T&) when the parameter is required (cannot be null) and the function does not take ownership. Use a pointer (T*) when the parameter is optional (nullptr signals absence) or when the function takes ownership and will store or delete it. Use std::unique_ptr when the function takes exclusive ownership; std::shared_ptr when ownership is shared. Use const T& for read-only large parameters to avoid copying. Use T& for read-write parameters where reassignment is needed. References cannot be reseated to a different object after initialization; pointers can be.
Can you help with CS50, CS61C, or CMU 15-213 pointer assignments?
Yes. CS50 problem sets 4 (Memory) and 5 (Speller) cover image manipulation in C with manual malloc and free, plus a trie implementation. CS61C project 1 is a linked-list project in C with Valgrind grading for memory correctness. CMU 15-213 malloc lab implements a custom heap allocator (implicit free list, explicit free list, or segregated fits); the attacklab exploits buffer overflows via crafted pointer arithmetic. Our tutors deliver implementations with explicit pointer-rewiring ASCII diagrams for linked-list operations, Valgrind reports proving zero leaks, and gdb session logs for the buffer-overflow attacks.

Stuck on pointers?

Submit your assignment and get expert, pedagogical help within 12 hours. Every solution ships with line-by-line comments, complexity analysis, and unlimited revisions.

Get Pointers Help