Functions: declaration, definition & argument passing
A function bundles up code behind a name, takes typed parameters, and returns a result. The task is identical across all seven: define max(a, b) returning the larger int, then call it on 7 and 3. Watch how arguments travel: integers here are passed by value (copied onto the callee's stack frame), so the function never touches the caller's storage and there is nothing to allocate or free - the contrast with by-reference/pointer passing (where the callee can mutate or must not outlive the caller's data) is exactly where memory bugs begin.
#include <stdio.h>
/* Declaration (prototype): tells the compiler the signature. */
int max(int a, int b);
/* Definition: the actual body. a and b are *copies* of the args. */
int max(int a, int b) {
return (a > b) ? a : b;
}
int main(void) {
int m = max(7, 3); /* 7 and 3 are copied into a and b */
printf("%d\n", m); /* prints 7 */
return 0;
}C separates the declaration (prototype int max(int, int);) from the definition (the body). Scalars like int are passed by value - a and b are independent copies on max's stack frame, so there is no heap and nothing to free; to mutate a caller's variable you would pass a pointer (int *) instead.
#include <iostream>
// A function template: `max` works for any comparable type, resolved
// at compile time. constexpr lets it run at compile time too.
template <typename T>
constexpr T max(T a, T b) {
return (a > b) ? a : b;
}
int main() {
int m = max(7, 3); // T deduced as int; 7 and 3 copied by value
std::cout << m << '\n'; // prints 7
}Modern C++ generalizes with a function template: max<T> is stamped out per type at compile time, and constexpr allows evaluation during compilation. int parameters are passed by value (cheap copies); large objects would instead take const T& to pass by reference and avoid copying without transferring ownership.
// Top-level code runs in TempleOS - no main() needed.
// HolyC supports default args: max(7) would use b=0.
I64 Max(I64 a, I64 b=0) {
return a > b ? a : b; // a and b are by-value copies
}
I64 m = Max(7, 3);
Print("%d\n", m); // prints 7
HolyC functions look C-like but add default arguments (b=0) and may be called at the top level with no main. I64 parameters are passed by value onto the stack - no allocation is involved, so there is nothing to Free; passing an I64 * would let the callee write back into the caller's storage.
const std = @import("std");
// Plain function: parameters are immutable, passed by value.
// `comptime T` would make it generic; here it's a concrete i32.
fn max(a: i32, b: i32) i32 {
return if (a > b) a else b;
}
pub fn main() void {
const m = max(7, 3); // 7 and 3 copied into a, b
std.debug.print("{d}\n", .{m}); // prints 7
}Zig declares functions with fn name(args) ReturnType. Parameters are immutable and passed by value (Zig may pass larger ones by const reference internally, but you cannot mutate them) - no allocator is touched, so there is nothing to free. To return an error you would write the return type as !i32; to be generic, take a comptime T: type.
use fmt;
// fn defines a function; parameters are immutable, passed by value.
fn max(a: int, b: int) int = {
return if (a > b) a else b;
};
export fn main() void = {
const m = max(7, 3); // 7 and 3 copied into a, b
fmt::println(m)!; // prints 7
};Hare uses fn name(args) ret = { ... };, and export fn main is the entry point. int parameters are passed by value (copies on the stack), so no alloc/free is needed; mutating a caller's value requires passing a pointer *int. The trailing ! on println is the error-assertion operator: it aborts the program if printing fails (whereas ? would propagate the error to the caller).
package main
import "core:fmt"
// proc defines a procedure; the return type follows ->.
// Parameters are immutable and passed by value.
max :: proc(a: int, b: int) -> int {
return a if a > b else b
}
main :: proc() {
m := max(7, 3) // 7 and 3 copied into a, b
fmt.println(m) // prints 7
}Odin spells functions name :: proc(args) -> ret. Parameters are immutable and passed by value, so the implicit context.allocator is never used - there is nothing to free. Returning or mutating heap data would involve the allocator; passing ^int lets a proc write back into the caller's storage.
\ A `word` is Forth's function: it takes/returns values on the data stack.
\ No named parameters -- arguments are whatever is already on the stack.
: MAX ( a b -- max ) \ stack effect: consumes two, leaves the larger
2DUP < IF SWAP THEN ( a b -- big small ) \ ensure top is the smaller
DROP ( big small -- big ) \ discard the smaller
;
7 3 MAX ( push 7 and 3, then MAX leaves 7 )
. CR ( . prints and consumes top -> 7 )Forth has no named parameters or return: a word like MAX pops its arguments off the data stack and pushes its result, documented by the ( a b -- max ) stack-effect comment. Everything lives on the stack, so there is no heap allocation and nothing to free. (Standard Forth already provides MAX; this spells it out to show the call mechanics.)