When a function is called, values are passed from the caller to the function parameters.
When a function is called, values are passed from the caller to the function parameters.
Consider:
const std = @import("std");
fn addOne(x: i32) i32 {
return x + 1;
}
pub fn main() void {
var n: i32 = 5;
const m = addOne(n);
std.debug.print("n = {d}, m = {d}\n", .{ n, m });
}The output is:
n = 5, m = 6The value of n is copied into the parameter x.
Changing x does not change n.
This is called passing by value.
The parameter receives its own local copy.
Even when the original variable is mutable, the parameter itself is immutable:
fn f(x: i32) void {
// x += 1; illegal
}To modify data belonging to the caller, a pointer must be passed.
Here is a function that changes a variable through a pointer:
const std = @import("std");
fn increment(x: *i32) void {
x.* += 1;
}
pub fn main() void {
var n: i32 = 5;
increment(&n);
std.debug.print("{d}\n", .{n});
}The output is:
6The parameter type is:
*i32which means “pointer to i32”.
The expression:
&nproduces the address of n.
Inside the function:
x.*means “the value pointed to by x”.
The statement:
x.* += 1;modifies the original variable.
Small values are usually passed directly:
fn square(x: i32) i32 {
return x * x;
}This is simple and efficient.
Larger objects are often passed by pointer:
const Buffer = struct {
data: [1024]u8,
};
fn clear(buf: *Buffer) void {
for (&buf.data) |*byte| {
byte.* = 0;
}
}Passing a pointer avoids copying the entire object.
Arrays behave differently from slices.
This function takes a fixed-size array:
fn printArray(a: [4]i32) void {
_ = a;
}The size is part of the type.
Only arrays of exactly four elements may be passed.
Slices are more flexible:
fn printSlice(values: []const i32) void {
_ = values;
}A slice contains:
- a pointer
- a length
Slices allow functions to operate on arrays of many sizes.
Strings are usually passed as slices:
fn greet(name: []const u8) void {
std.debug.print("hello, {s}\n", .{name});
}The type:
[]const u8means “read-only slice of bytes”.
A function may return a pointer, but care is required.
This is wrong:
fn bad() *i32 {
var x: i32 = 10;
return &x;
}x stops existing when the function returns. The returned pointer becomes invalid.
This kind of mistake is common in systems programming. Zig tries to make ownership and lifetimes explicit.
Functions may take pointers that cannot modify data:
fn printNumber(x: *const i32) void {
std.debug.print("{d}\n", .{x.*});
}*const i32 means the pointed-to value is read-only through this pointer.
The caller may still own mutable data, but the function promises not to modify it.
A good rule is:
- pass small values directly
- pass large objects by pointer
- use
constwhenever mutation is unnecessary
This keeps interfaces simple and predictable.
Exercise 4-9. Write a function swap that exchanges two integers using pointers.
Exercise 4-10. Write a function that sets every element of a slice to zero.
Exercise 4-11. Why is returning a pointer to a local variable unsafe?
Exercise 4-12. Change increment so it cannot modify the pointed-to value. What compiler error results?