In a high-level language, a pointer is just a reference. In C and C++, a pointer is a memory address, and how you manipulate that address can either make your driver highly efficient or cause a hardware “Bus Error” that is nearly impossible to debug. For a System Architect, understanding Memory Alignment and Strict Aliasing is non-negotiable.
1. The Mechanics of Pointer Arithmetic
The most common mistake junior engineers make is assuming ptr + 1 always adds one byte to the address.
-
The Rule: Adding
nto a pointer of typeT*increments the address by $n \times \text{sizeof}(T)$. -
Example: On a 32-bit system:
-
uint8_t *p8; p8 + 1;→ Adds 1 byte. -
uint32_t *p32; p32 + 1;→ Adds 4 bytes. -
void *pv; pv + 1;→ Undefined Behavior (though many compilers treat it as 1 byte, it is non-standard).
-
2. The Alignment Trap: Why 0x1001 is Dangerous
In the semiconductor world, hardware usually expects data to be “naturally aligned.” A 32-bit integer should start at an address divisible by 4.
The Scenario: You are parsing a packet from a network buffer.
uint8_t buffer[10] = {0x00, 0xAA, 0xBB, 0xCC, 0xDD, ...};
uint32_t *val = (uint32_t *)&buffer[1]; // Unaligned access!
-
x86 Architecture: Usually handles this in hardware with a slight performance penalty.
-
ARM/RISC-V: Depending on the configuration, this might trigger an Alignment Fault (UsageFault), crashing the system instantly.
The Architect’s Solution: Use memcpy or __attribute__((packed)) to handle unaligned data, or better yet, design your hardware structures to be naturally aligned from the start.
3. Type Punning and Strict Aliasing
“Type Punning” is the act of accessing the same memory location as two different types. While common in embedded code, it can trigger Strict Aliasing optimizations that break your logic.
The Danger:
void swap_halves(uint32_t *ptr) {
uint16_t *half = (uint16_t *)ptr;
// The compiler may assume *ptr and *half never point to the same memory
// and reorder reads/writes in a way that breaks your logic.
}
Modern compilers (GCC/Clang with -O2 or -O3) assume that pointers of different types do not alias. To safely type-pun, architects use a union or memcpy.
4. The “Architect’s Question”: void* vs uintptr_t
Q: When should you use uintptr_t instead of void*?
A: Use void* when you want to point to “opaque” data that the CPU will eventually dereference. Use uintptr_t (from <stdint.h>) when you need to perform arithmetic on addresses (like calculating a page offset or masking bits). uintptr_t is an unsigned integer guaranteed to be large enough to hold a pointer, making it safe for bitwise operations like addr & ~0xFFF.
5. Summary Table: Pointer Operations
| Operation | Best Type to Use | Why? |
| Passing data to a function | void* |
Generic, hides internal structure. |
| Stepping through a byte array | uint8_t* or char* |
Increments by exactly 1 byte. |
| Bitwise address masking | uintptr_t |
Integers allow &, ` |
| Direct Hardware Register Access | volatile uint32_t* |
Ensures every read/write hits the silicon. |
Architect’s Interview Tip
If asked to parse a binary header, don’t just cast a pointer to a struct. Mention that you are aware of Endianness and Padding. A struct on a 64-bit system might have different internal padding than on a 32-bit system. Using __attribute__((packed)) or explicit padding members shows you design for portability across different silicon.
In the next article, we look at the “hidden” side of variables: The const Qualifier, constexpr, and the Symbol Table.
Ready for Article 4?
