Overview
Passing a pointer (an address) to a function lets the callee access the caller’s storage. This enables in-place updates, out-parameters, and zero-copy APIs - but introduces hazards: null/dangling pointers, aliasing, lifetime, and bounds issues. This note gives a practical guide to pointer-parameter design: when to use pointers, how to express read-only vs read–write intent (const-correctness), and how to avoid common bugs.
Note
Think of a pointer parameter as a capability: “here is where you may read/write.” Good APIs make that capability explicit (read-only vs mutable, length, ownership).
Underlying Process
At a call site:
-
The caller computes the address of an object (stack, heap, global) and passes that address.
-
The callee receives the address as a parameter (e.g.,
int *p). -
Reads/writes use indirection (
*p,p[i]) to access the caller’s storage. -
On return, any writes through the pointer are visible to the caller; the pointer value itself (an integer-like address) was passed by value unless explicitly passed by reference-to-pointer.
Const-correctness
-
const T* p- callee may read*pbut must not modify it. -
T* p- callee may read and write*p. -
T* const p- the pointer variablepitself cannot be reassigned, but*pmay change. -
const T* const p- fixed pointer to read-only data.
Tip
Prefer
constparameters for inputs. Reserve mutable pointers for explicit out or in/out roles.
Example Execution
1) Out-parameter for status + result (C-style)
// Returns 0 on success; writes result into *out_sum.
int sum_u32(const uint32_t* arr, size_t n, uint64_t* out_sum) {
if (!arr || !out_sum) return -1;
uint64_t s = 0;
for (size_t i = 0; i < n; ++i) s += arr[i];
*out_sum = s; // write into caller's memory
return 0;
}
// Caller:
uint64_t s;
if (sum_u32(a, n, &s) == 0) { /* use s */ }-
arris read-only viaconst. -
out_sumis write-only in spirit; it should be documented as such.
2) In-place transform on a buffer
void scale_in_place(float* xs, size_t n, float c) {
if (!xs) return;
for (size_t i = 0; i < n; ++i) xs[i] *= c;
}- The function mutates the caller’s array via
xs[i].
3) Reference-to-pointer when callee must rebind storage
// Reallocate and update caller's pointer.
// Returns new length; sets *buf to new storage or NULL on failure.
size_t ensure_capacity(uint8_t** buf, size_t* cap, size_t need) {
if (*cap >= need) return *cap;
size_t new_cap = (*cap ? *cap * 2 : 64);
while (new_cap < need) new_cap *= 2;
uint8_t* p = (uint8_t*)realloc(*buf, new_cap);
if (!p) { /* leave old buffer intact */ return *cap; }
*buf = p; // rebind caller's pointer
*cap = new_cap;
return new_cap;
}- Pass
T**(pointer to pointer) when the callee must change where the caller points.
Performance and Design Trade-offs
-
Zero-copy: Pointers avoid copying large data structures (
O(1)passing). -
In-place vs out-of-place: In-place can be faster and memory-frugal, but harder to reason about. Out-of-place (returning a new value) is safer and often clearer.
-
Cache locality: Mutating through pointers is as local as your data layout; see Dynamic Arrays and Multidimensional Arrays for stride/layout implications.
-
API clarity: Use names and types to signal direction:
-
Inputs:
const T* in,size_t in_len -
Outputs:
T* out,size_t out_cap(capacity), return actual bytes written. -
In/out:
T* inout, document changes.
-
Tip
For bulk writes, pass capacity + length: the callee can avoid overflow and the caller can pre-size buffers.
Correctness and Reliability Considerations
1) Bounds and length
A pointer does not carry length. Always pair buffer pointers with explicit size (n) or use a slice/view type ({ptr,len}).
int fill(uint8_t* out, size_t cap) {
if (cap < 4) return -1; // guard
out[0]=1; out[1]=2; out[2]=3; out[3]=4;
return 4;
}2) Null, dangling, and lifetime
-
Null: check for
NULL(or forbid it contractually). -
Dangling: never return pointers to stack locals; ensure the pointee outlives its uses.
-
Reallocation: functions like
realloccan move storage - callers must update all aliases.
3) Aliasing and reentrancy
Two different pointer parameters can alias the same region, causing subtle bugs if written in the wrong order.
// Safe even when dst==src: iterate from end on overlap.
void memmove_like(uint8_t* dst, const uint8_t* src, size_t n) {
if (dst < src) for (size_t i=0;i<n;++i) dst[i]=src[i];
else if (dst > src) for (size_t i=n; i>0; --i) dst[i-1]=src[i-1];
}4) Const-correctness discipline
-
Mark inputs
const. -
Use
constto allow aliasing safely (compiler can assume read-only). -
Do not cast away
constunless the original object was non-const and you control its lifetime.
5) Ownership and allocation
Document who allocates and who frees:
-
Caller allocates, callee fills (classic out-param).
-
Callee allocates, caller frees (return pointer or
T** out). -
Shared ownership requires reference counts or conventions - avoid in low-level C unless necessary.
Warning
Mismatched allocator/free (
mallocvs custom pool) leads to crashes. Free with the same allocator family that allocated.
6) Thread-safety
Pointers cross thread boundaries as raw capabilities. If multiple threads write through aliases, use locks or restrict to immutable reads.
7) Language notes (brief)
-
C/C++: raw pointers as above; prefer
span<T>/std::span(C++) for{ptr,len}views; preferT&/const T&for single objects when mutation is controlled. -
Go: pass
*Tfor mutating a struct; slices and maps are reference-like already - passing a slice by value shares backing storage. -
Rust: uses borrows instead of raw pointers in safe code (
&T,&mut T) with compile-time lifetime checks; raw pointers exist inunsafe. -
Java/C#/Swift: ordinary references act as pointers to objects; arrays are reference types (mutations visible). Interop with native code uses explicit pointer wrappers.
Implementation Notes
-
Prefer half-open ranges (
[0..n)) in loops to avoid off-by-one errors when indexing throughp[i]. -
Avoid double free by centralizing ownership responsibility; use RAII in C++ and “free-after-transfer” patterns in C.
-
Validate alignment if the callee performs vectorized loads/stores; misaligned pointers can hurt performance or fault on strict platforms.
-
For binary protocols, define structs with explicit packing or use byte-wise access to avoid padding/endianness pitfalls.
Tip
Introduce lightweight view types early (
struct slice { void* ptr; size_t len; }). They document bounds and make APIs harder to misuse than rawT*.
Related Concepts
-
References vs pointers: references (
T&) are non-null, aliasing handles with simpler syntax; pointers (T*) can be null/reassigned. -
Handles/IDs: instead of exposing pointers, some APIs pass opaque IDs that the callee resolves internally - safer but less flexible.
-
Copy vs move: for large data, consider move (transfer ownership) rather than pointer mutation; see Pass-by-Value vs Reference.
Summary
Pointers-as-parameters are a powerful capability: they enable in-place updates, zero-copy I/O, and flexible memory management. Use them deliberately:
-
Mark inputs
const, reserve mutable pointers for out or in/out roles. -
Always pair pointers with length/capacity and document ownership.
-
Guard against null, dangling, aliasing, and bounds errors.
-
When the callee must rebind the caller’s pointer, pass a pointer to pointer (
T**) (or equivalent in your language). With these habits, pointer-based APIs are both fast and safe enough for systems work and high-performance DS&A code.