Chapter 7
References, Pointers, and Memory Management
Bachelors Program in Electrical Engineering
Programming Fundamentals for Engineers
•
Bachelor
References, Pointers, and Memory Management
Introduction
Programs do not operate on abstract variables alone. During execution, instructions and data occupy memory locations, and the processor moves values between registers, memory, and peripheral devices. Understanding how values are stored, how their addresses are represented, and how long storage remains valid is therefore essential for systems programming, embedded systems, operating systems, robotics, and performance-critical engineering software.
A reference or pointer provides an indirect way to access an object. Indirection is powerful: it supports linked data structures, arrays, device registers, dynamic data, callbacks, and efficient parameter passing. It is also a major source of defects. A pointer may contain an invalid address, refer to an object whose lifetime has ended, or provide access to storage outside the intended bounds.
This chapter develops a language-aware but broadly applicable model. Examples use C-like notation because C and C++ expose memory operations explicitly. Exact details vary among programming languages and implementations; a conceptual memory address is not always directly observable or portable.
Learning Objectives
After studying this chapter, you should be able to:
Describe a typical process memory layout and distinguish stack, heap, static, and code regions.
Explain addresses, references, pointers, dereferencing, and pointer arithmetic.
Compare automatic and dynamic storage allocation.
Analyze object lifetime, ownership, aliasing, and resource release.
Identify common memory-safety defects and select appropriate prevention techniques.
Apply these ideas to an engineering programming problem involving bounded data and dynamic resources.
1. Memory as a Structured Address Space
A running program is given a virtual address space. An address is an identifier used by the execution environment to locate a byte or object. Modern systems normally translate virtual addresses into physical memory through hardware and operating-system mechanisms. Consequently, an address printed by a program is usually meaningful only within that process and execution context.
A typical process contains the regions shown below.
Typical process memory layout showing code, static data, heap, and stack
Figure: Typical process memory layout showing code, static data, heap, and stack
Figure: A simplified process memory layout. Actual placement and growth directions depend on the operating system, architecture, compiler, and runtime.
The code region contains executable instructions and may be marked non-writable. Static and global objects exist for most or all of the program's execution. The stack usually stores function-call frames, including parameters, return information, and automatic local objects. The heap is a region from which a program or runtime obtains storage dynamically.
This diagram is conceptual rather than a hardware specification. Threads commonly have separate stacks, shared heap storage, and implementation-dependent arrangements. Memory-mapped files and devices may occupy additional regions.
Bytes, objects, and alignment
Memory is byte-addressable on most modern systems: each byte has an address, although larger objects occupy several consecutive bytes. If an object begins at address a and has size n bytes, its occupied byte addresses are commonly a through a+n-1, assuming contiguous storage and no unusual representation.
A type determines how a sequence of bits is interpreted. For example, the same bits could represent an integer, a floating-point value, or part of a character sequence. Alignment requirements may require an object to begin at an address divisible by a type-specific boundary. Compilers may insert padding between structure members to satisfy alignment, so the size of a structure is not necessarily the sum of its visible field sizes.
2. Variables, Addresses, References, and Pointers
A variable is a named object or storage location associated with a type. An address-of operation obtains a pointer to an object. In C-like notation:
int voltage = 12;
int *p = &voltage;
int copy = *p;
Here, &voltage produces the address of voltage, p stores that address, and *p dereferences the pointer to access the pointed-to integer. The assignment to copy reads the value; it does not make copy an alias of voltage.
A pointer has two conceptually separate properties: the address it contains and the type of access permitted through it. A null pointer represents no valid target and must not be dereferenced. An uninitialized pointer contains an indeterminate value and must not be used. A pointer can also be made invalid when its target is released or leaves scope.
A reference, in languages such as C++, is an alias bound to an existing object. For example:
int current = 4;
int& alias = current;
alias = 9; // current is now 9
A reference is generally required to refer to an object, whereas a pointer can be null and can be reseated to refer to different objects. Language rules differ, so “reference” may have a different formal meaning in Java, C#, Rust, or a managed runtime.
Passing data to functions
A function parameter passed by value receives a copy. Changes to the parameter do not change the caller's object. Passing a pointer or reference allows a function to modify the caller's object, provided access is valid and the interface permits mutation.
The null check prevents dereferencing a null pointer, but it does not prove that a non-null pointer is valid. Validity also requires that the object exists, is appropriately aligned, has a compatible type, and is accessible for the requested operation.
Pointer arithmetic and arrays
In C, an array occupies contiguous elements. If p points to an element of an array, p + 1 points to the next element, scaled by the size of the pointed-to type. It does not necessarily advance by one byte. Pointer arithmetic is defined within the same array object, with a special one-past-the-end position permitted for comparison and iteration but not dereferencing.
int samples[4] = {3, 5, 7, 9};
int *p = samples;
for (int i = 0; i < 4; ++i) {
process(p[i]);
}
The loop accesses indices 0 through 3. Accessing samples[4] is out of bounds, even if the resulting address happens to lie in readable memory. Readability does not establish ownership or logical validity.
3. Storage Duration and Resource Lifetime
Storage duration describes how long an object exists. Three important categories are:
Storage category
Typical creation
Typical end of lifetime
Common use
Automatic
Entering a block or function
Leaving the block
Temporary local variables
Static
Program or module initialization
Program termination
Global and static state
Dynamic
Explicit allocation or runtime mechanism
Explicit release or garbage collection
Variable-size data
An automatic object is convenient and efficient, but returning its address is unsafe:
int *bad(void) {
int result = 42;
return &result; // invalid after the function returns
}
When the function returns, result no longer exists. The returned pointer is dangling. The old bytes may remain unchanged temporarily, but using the pointer has undefined behavior in C and is invalid in any sound memory model.
Dynamic storage has a different lifetime. In C, malloc reserves a block and free releases it. In C++, new and delete or, preferably, standard resource-managing types may be used. A dynamically allocated object remains alive until its release operation or until a managed runtime reclaims it.
A useful engineering rule is: every resource should have a clearly identifiable owner and a clearly defined release path. “Resource” includes memory, file handles, sockets, locks, DMA buffers, and peripheral reservations—not only heap blocks.
4. Stack and Heap Allocation
Stack allocation is normally associated with function calls. It is fast because allocation often consists of moving a stack pointer, and reclamation occurs automatically when a call returns. However, stack capacity is limited, and deeply recursive calls or very large local arrays can cause stack overflow.
Heap allocation supports data whose size or lifetime is not known at compile time. It can support objects that outlive the function that created them, but allocation and release are more costly and can fail. Repeated allocation and release may fragment the heap, leaving sufficient total free memory but no suitably large contiguous block.
Worked example: choosing storage
Suppose an embedded controller samples 64 channels, each stored as a 16-bit integer. The raw buffer requires:
64 × 2 bytes = 128 bytes
This calculation assumes each channel uses exactly 2 bytes and excludes any metadata or alignment padding. A fixed-size local buffer may be appropriate if 128 bytes fits safely within the task's stack budget and the buffer's lifetime is limited to the sampling operation. A static buffer may be preferable when the task runs repeatedly and predictable allocation is required. A heap allocation is justified only when the number of channels varies or the buffer must have a dynamically determined lifetime.
In real-time and safety-critical systems, avoiding unpredictable heap operations during time-critical execution is common practice. Preallocated pools or fixed-size ownership structures provide more predictable behavior.
5. Dynamic Memory and Ownership
A dynamic allocation operation should be treated as a transaction with four questions:
Was allocation successful?
What object and size does the returned address represent?
Which component owns the allocation?
On every control-flow path, where is it released?
In C:
size_t n = 100;
int *buffer = malloc(n * sizeof *buffer);
if (buffer == NULL) {
report_error();
} else {
use(buffer, n);
free(buffer);
buffer = NULL;
}
The expression n sizeof buffer remains correct if the pointer's type changes. Production code must also check for integer overflow when calculating sizes from untrusted or very large inputs. A safe conceptual condition is that the requested element count must not exceed the maximum representable size divided by the element size. The exact maximum is represented by SIZE_MAX in C implementations that provide it.
Setting a pointer to null after free prevents that particular pointer from being accidentally reused, but it does not repair copies of the pointer held elsewhere. Ownership discipline is therefore more important than nulling alone.
Ownership models
Several models are widely used:
Single ownership: exactly one component releases the resource.
Borrowing: a component may use a resource temporarily but must not release it.
Shared ownership: several components participate in lifetime management, often through reference counting.
Region or arena ownership: many objects are released together when a region is destroyed.
Garbage collection: a runtime identifies unreachable objects and reclaims them.
Each model has trade-offs. Reference counting can leak cyclic structures unless weak references or cycle detection are used. Garbage collection reduces certain manual-release errors but may introduce pauses or memory-retention issues. Ownership systems such as Rust's borrow checker enforce many lifetime rules statically, but they require programmers to express ownership and borrowing precisely.
6. Aliasing and the Effects of Indirection
Aliasing occurs when two expressions can access the same object. In the following example, p and q alias value:
int value = 10;
int *p = &value;
int *q = &value;
*q = 20;
// *p is now 20
Aliasing is useful for sharing state, but it complicates reasoning. A function receiving two pointers may not know whether they refer to the same object. Reordering operations that appear independent can therefore change behavior. Compiler optimization rules also depend on language-specific aliasing rules; violating them can produce unexpected results even when machine-level addresses appear plausible.
A good interface documents whether parameters may alias, whether they are nullable, whether they are read-only, and who owns any returned resource. In C, const can communicate that a function will not modify an object through a particular pointer, although it does not establish ownership or thread safety.
Aliasing is especially dangerous in concurrent programs. If two threads access the same mutable object without an appropriate synchronization mechanism, a data race may occur. Memory safety and concurrency safety are related but distinct: a program can be free of out-of-bounds accesses yet still produce incorrect results because of unsynchronized aliasing.
7. Common Memory-Safety Defects
Null dereference
A null pointer is used as though it identified an object. Prevention includes explicit validation, non-null types where available, and interfaces that make absence explicit.
Uninitialized pointer or object
A pointer or value is read before it has been initialized. Initialization should occur at declaration or through a constructor, and compiler warnings should be treated seriously.
Buffer overflow and underflow
A program accesses before or after the bounds of an array or allocated block. The defect may corrupt data, control flow, or adjacent objects. Every access should be governed by a verified length, and APIs should carry both a pointer and its valid element count.
Use-after-free
An object is accessed after its storage has been released. This commonly arises when one alias outlives the owner. Clear ownership, RAII, garbage collection, or borrow checking reduces the risk.
Double release
The same resource is released twice. This can corrupt an allocator or other resource manager. Centralized ownership and idempotent cleanup strategies help prevent it.
Memory leak
A program loses the ability to release an allocated object while the object remains allocated. Leaks reduce available memory and may eventually cause failure. Leak detection tools and scope-based ownership are effective countermeasures.
Invalid free
A release operation is applied to a nonallocated address, an interior address, or a block already released. Only the allocating subsystem and correct base address should be used for release.
Lifetime and type violations
An object may be accessed after its lifetime ends or through an incompatible type. These errors are often harder to see than simple bounds errors because the bytes may look correct in a particular build.
A defect is not made safe by the fact that it “works on the current machine.” Undefined behavior means the language implementation is not required to preserve the observed result. Optimization level, compiler version, hardware, and unrelated code can expose the failure.
8. Tools and Defensive Practice
Professional development combines design discipline with automated analysis.
Enable compiler warnings and treat high-value warnings as errors.
Use address sanitizers to detect many out-of-bounds and use-after-free defects during testing.
Use undefined-behavior sanitizers where supported.
Use leak detectors, static analyzers, and code review.
Test boundary sizes, zero-length inputs, allocation failure, repeated cleanup, and error paths.
Prefer library abstractions that carry size and ownership information.
Keep pointer scopes short and avoid unnecessary global mutable state.
Separate parsing from processing and validate lengths before copying.
Document whether a function borrows, transfers, or shares ownership.
Sanitizers are not proofs of correctness. They detect executed failures, not every possible failure. Static analysis can reason about paths but may produce false positives or miss complex behavior. Formal verification and memory-safe languages provide stronger guarantees for selected classes of systems, although interfaces with hardware and unsafe code still require care.
Security is a central concern. Memory-safety flaws can enable data disclosure, denial of service, or control-flow hijacking. Defenses such as non-executable memory, address-space layout randomization, control-flow integrity, and hardware-enforced bounds improve resilience, but they do not replace correct programming.
9. Applied Case: Dynamic Sensor Records
Background
Consider an illustrative industrial monitoring controller. It receives packets containing a sensor identifier, a sample count, and a sequence of signed measurements. The number of measurements varies from packet to packet.
Problem
An early implementation allocated storage based on the packet's count field and copied incoming bytes into the allocation. It used a 32-bit multiplication for the requested size and assumed that the packet was always well formed. A second function retained a pointer to the record after the first function released it.
The design contained two vulnerabilities: an integer-size calculation could wrap and allocate too little memory, and a dangling pointer could later be dereferenced.
Analysis
The correct allocation size must account for the number of elements, the representation size of each element, and any record metadata. The count must be validated against both the packet's actual remaining length and the maximum permitted system count before multiplication. Ownership must be assigned: either the record manager owns the allocated record until consumers finish, or consumers receive a safe borrowed view whose lifetime is bounded by the manager's scope.
Approach and solution
A robust redesign uses the following sequence:
Verify that the packet header is present.
Decode the count using a defined byte order.
Reject counts above the application limit.
Verify that the encoded payload contains the required number of bytes.
Check the size calculation before allocation.
Allocate through a single record-construction function.
Transfer ownership explicitly to a queue, or copy the data into a queue-owned object.
Release the record exactly once after all consumers finish.
Run tests with zero, maximum, truncated, and oversized counts under sanitizers.
Outcome
The redesigned system rejects malformed packets before allocation, avoids arithmetic wraparound, and makes the record queue the single owner. Consumers borrow the record only while processing the queue item. This improves safety, simplifies cleanup, and makes failure behavior observable.
Learning points
Memory safety is primarily a design property. Bounds checks, allocation checks, lifetime rules, and ownership documentation must work together. No single null check or sanitizer can compensate for an unclear ownership model.
Challenges and Limitations
Manual memory management offers control and predictable overhead but demands rigorous discipline. Automatic and garbage-collected approaches reduce some defects but may introduce runtime costs, nondeterministic reclamation, or restrictions on hardware interaction. Smart pointers reduce leaks but can create cycles, hidden ownership, or excessive reference-count traffic. Static ownership systems improve guarantees but may require redesign of legacy interfaces.
Engineers must also account for fragmentation, stack limits, cache behavior, alignment, interrupt contexts, real-time deadlines, and multi-threaded access. A technically valid allocation may still be unacceptable if it violates timing, energy, or certification constraints.
Chapter Summary
Memory is a structured address space containing code, static data, automatic storage, dynamic storage, and often memory-mapped resources. Pointers store addresses and provide indirect access; references provide language-specific aliasing mechanisms. Dereferencing is valid only when the target exists, is correctly aligned and typed, and is within its permitted bounds.
Stack storage is convenient and automatically reclaimed, while heap storage supports variable size and extended lifetime at the cost of allocation complexity and possible fragmentation. Ownership determines who may release a resource, and lifetime determines when access remains valid. Aliasing enables sharing but complicates analysis and concurrency.
Null dereferences, uninitialized values, buffer overflows, use-after-free, double release, leaks, invalid release, and lifetime violations are common memory-safety defects. Defensive design combines explicit ownership, checked sizes, bounded interfaces, compiler diagnostics, testing, sanitizers, static analysis, and—where appropriate—memory-safe languages and hardware protections.
Key Terms
Address: A value used to identify a location or object in an address space.
Aliasing: The condition in which multiple expressions can access the same object.
Automatic storage: Storage associated with a block or function and reclaimed when its scope ends.
Dangling pointer: A pointer whose former target no longer exists or is no longer valid.
Dereference: An operation that accesses the object designated by a pointer.
Dynamic allocation: Obtaining storage during program execution.
Heap: A region or mechanism used for dynamically managed storage.
Lifetime: The interval during which an object exists and may be accessed.
Memory leak: Allocated storage that remains unavailable because the program lost its release capability.
Null pointer: A pointer value that designates no valid object.
Ownership: Responsibility for controlling a resource's use and release.
Pointer: An object that stores an address or address-like reference to another object.
Reference: A language-defined alias or access mechanism bound to an object.
Stack: Storage commonly used for active function calls and automatic objects.
Static storage: Storage whose lifetime generally extends from initialization to program termination.
Use-after-free: Access to storage after it has been released.
Buffer overflow: Access beyond the upper bound of a buffer.
Resource lifetime: The period from acquisition of a resource until its release or reclamation.
Review Questions
Short Answer Questions
What is the difference between a pointer and the object to which it points?
Why is returning the address of an automatic local variable unsafe?
What is aliasing, and why can it complicate optimization?
Name two differences between stack and heap storage.
Why must a dynamic allocation request be checked for failure?
Long Answer Questions
Explain a typical process memory layout and discuss why the layout is implementation-dependent.
Compare single ownership, shared ownership, borrowing, and garbage collection.
Explain how pointer arithmetic relates to arrays and why one-past-the-end pointers cannot be dereferenced.
Discuss the causes and prevention of use-after-free and double-release defects.
Explain how compiler diagnostics, sanitizers, static analysis, and testing complement one another.
Analytical / Application Questions
Design an ownership and lifetime policy for a dynamically allocated queue of sensor records shared by a producer and several consumers.
A packet declares 10,000 elements, but the packet contains only enough payload bytes for 100 elements. Identify the checks required before allocation and copying.
An embedded task has a small fixed stack and a hard real-time deadline. Evaluate whether a heap allocation inside its periodic loop is appropriate, and propose alternatives.
Multiple Choice Questions with Answers
Question 1
Which operation obtains the address of an object in C-like notation?
A. *x
B. &x
C. x++
D. sizeof(x)
Correct Answer: B. &x
Question 2
What happens to an automatic local variable when its function returns?
A. Its lifetime normally ends
B. It is moved automatically to the heap
C. It becomes a static object
D. It remains valid through all saved pointers
Correct Answer: A. Its lifetime normally ends
Question 3
Which statement about a one-past-the-end pointer is correct?
A. It may always be dereferenced safely
B. It is valid only for heap arrays
C. It may be used for comparison or iteration but not dereferenced
D. It is equivalent to a null pointer
Correct Answer: C. It may be used for comparison or iteration but not dereferenced
Question 4
Which defect occurs when allocated storage is accessed after free?
A. Memory leak
B. Use-after-free
C. Integer promotion
D. Static initialization
Correct Answer: B. Use-after-free
Question 5
What is the main purpose of assigning NULL to a pointer after freeing its target?
A. It restores the freed object
B. It prevents all aliases from being used
C. It reduces the chance of reusing that particular pointer accidentally
D. It increases heap capacity
Correct Answer: C. It reduces the chance of reusing that particular pointer accidentally
Question 6
Which storage category is typically reclaimed when a function block is exited?
A. Automatic storage
B. Static storage
C. Read-only code storage
D. Memory-mapped device storage
Correct Answer: A. Automatic storage
Question 7
Two pointers alias when they:
A. Have different types
B. Contain null values
C. Can access the same object
D. Point to different array elements
Correct Answer: C. Can access the same object
Question 8
Why should a program validate an element count before calculating count × sizeof(element)?
A. To guarantee stack allocation
B. To prevent an invalid or overflowing size calculation
C. To convert the element type
D. To eliminate alignment requirements
Correct Answer: B. To prevent an invalid or overflowing size calculation
Question 9
Which approach gives a component temporary access without making it responsible for releasing the resource?
A. Borrowing
B. Double release
C. Invalid free
D. Memory corruption
Correct Answer: A. Borrowing
Question 10
A 64-channel buffer uses 2 bytes per channel. Ignoring metadata and padding, how many bytes are required?
A. 32 bytes
B. 64 bytes
C. 128 bytes
D. 256 bytes
Correct Answer: C. 128 bytes
Programming Fundamentals for Engineers
Chapter 7
of 15