← Code Compare

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.

Show: CC++HolyCZigHareOdinForth
C
#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.

C++
#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.

HolyC
// 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.

Zig
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.

Hare
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).

Odin
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.

Forth
\ 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.)