Zig gives the programmer direct access to memory, integers, pointers, and machine operations. This makes many programs simple and efficient. It also makes some operations...
Zig gives the programmer direct access to memory, integers, pointers, and machine operations. This makes many programs simple and efficient. It also makes some operations dangerous.
An operation whose result is not defined by the language is called undefined behavior.
Undefined behavior means the compiler is allowed to assume the operation never happens. If it does happen, the result is unpredictable. The program may crash. It may appear to work. It may corrupt memory silently.
A simple example is reading past the end of an array:
const std = @import("std");
pub fn main() void {
const values = [_]u8{ 10, 20, 30 };
std.debug.print("{d}\n", .{values[5]});
}The array has three elements:
[_]u8{ 10, 20, 30 }Valid indexes are 0, 1, and 2.
The expression:
values[5]reads memory outside the array.
In safe build modes, Zig detects this and stops the program:
panic: index out of boundsIn unsafe modes, the compiler may remove checks and assume the index is valid.
The important point is not whether the program crashes. The important point is that the language no longer defines what happens.
Integer overflow is another example.
const std = @import("std");
pub fn main() void {
var x: u8 = 255;
x += 1;
std.debug.print("{d}\n", .{x});
}A u8 stores values from 0 to 255.
Adding 1 to 255 exceeds the range.
In safe modes, Zig detects the overflow:
panic: integer overflowIf wrapping behavior is wanted, it must be requested explicitly:
const std = @import("std");
pub fn main() void {
var x: u8 = 255;
x +%= 1;
std.debug.print("{d}\n", .{x});
}The operator:
+%=means wrapping addition.
The result is:
0Zig separates checked arithmetic from wrapping arithmetic. Overflow is not silently ignored.
Pointer misuse is another source of undefined behavior.
const std = @import("std");
pub fn main() void {
var x: i32 = 10;
const ptr: *i32 = &x;
ptr.* = 20;
std.debug.print("{d}\n", .{x});
}This is valid. ptr points to a real object.
But a pointer can also be created incorrectly:
const bad: *i32 = @ptrFromInt(1);This creates a pointer from an arbitrary integer.
Dereferencing such a pointer is undefined unless the address is actually valid memory of the correct type and alignment.
Many undefined operations involve assumptions about memory layout.
For example:
const std = @import("std");
const S = struct {
a: u8,
b: u32,
};
pub fn main() void {
var s = S{
.a = 1,
.b = 2,
};
const p: *u32 = @ptrCast(&s.a);
std.debug.print("{d}\n", .{p.*});
}The address of s.a is not necessarily aligned for u32.
Dereferencing p may fail on some systems.
Alignment matters because many CPUs require certain values to begin at certain memory boundaries.
Zig checks alignment in safe modes.
Undefined behavior also appears when using uninitialized memory.
const std = @import("std");
pub fn main() void {
var x: i32 = undefined;
std.debug.print("{d}\n", .{x});
}The value of x is undefined.
The program must assign a real value before reading it.
undefined is not zero. It is not random. It means the value is unspecified and should not be used.
Zig deliberately exposes undefined behavior instead of hiding it behind implicit conversions or silent runtime rules.
The language follows a simple principle:
Operations that are safe should work directly.
Operations that are unsafe should be explicit.
This is why Zig distinguishes:
| Operation | Meaning |
|---|---|
+ | Checked addition |
+% | Wrapping addition |
@ptrCast | Explicit pointer reinterpretation |
undefined | Uninitialized value |
@alignCast | Explicit alignment assertion |
The compiler assumes that undefined behavior never occurs. This allows better optimization and simpler generated code.
For example, if a loop index is guaranteed to stay inside array bounds, the compiler can remove repeated bounds checks.
The programmer gets both performance and safety checks during development.
The usual approach is:
| Build mode | Purpose |
|---|---|
| Debug | Maximum safety checking |
| ReleaseSafe | Optimized with safety checks |
| ReleaseFast | Maximum optimization |
| ReleaseSmall | Smaller binaries |
During development, safety checks help detect mistakes early. In production, the programmer decides how much checking to keep.
Undefined behavior cannot be eliminated from systems programming. Zig instead tries to make dangerous operations visible, explicit, and locally understandable.
Exercise 19-1. Write a program that triggers integer overflow with +, then rewrite it using +%.
Exercise 19-2. Create an array with four elements and intentionally access index 4. Observe the runtime behavior in Debug mode.
Exercise 19-3. Declare a variable with undefined, assign a value later, and print the final value.
Exercise 19-4. Use @ptrFromInt to construct a pointer. Explain why dereferencing it is dangerous.