Functions
Functions are the fundamental unit of code organization in both C and Rust. But the two languages differ in how they declare them, how they pass arguments, and how they organize code across files. This chapter covers all of it.
Declaring and Defining Functions in C
C distinguishes between a function declaration (prototype) and its definition (body).
/* functions_basic.c */
#include <stdio.h>
/* Declaration (prototype) */
int add(int a, int b);
int main(void)
{
int result = add(3, 4);
printf("3 + 4 = %d\n", result);
return 0;
}
/* Definition */
int add(int a, int b)
{
return a + b;
}
$ gcc -Wall -o functions_basic functions_basic.c && ./functions_basic
3 + 4 = 7
The declaration must appear before the first call. The definition can appear anywhere.
In C89, calling an undeclared function was allowed -- the compiler assumed int return.
Modern C (C99+) requires a declaration.
Caution: In older C code, you may see functions called without declarations. This is dangerous because the compiler cannot check argument types. Always use
-Wall -Wextra.
Defining Functions in Rust
Rust has no separation between declaration and definition. A function is defined once and can be called from anywhere in the same module, regardless of order.
// functions_basic.rs fn main() { let result = add(3, 4); println!("3 + 4 = {}", result); } fn add(a: i32, b: i32) -> i32 { a + b // no semicolon: this is the return expression }
The return type is specified with ->. If omitted, the function returns () (unit).
The last expression without a semicolon is the return value.
Parameter Passing: By Value
Both C and Rust pass arguments by value by default.
/* pass_by_value.c */
#include <stdio.h>
void increment(int x)
{
x = x + 1;
printf("inside: x = %d\n", x);
}
int main(void)
{
int a = 10;
increment(a);
printf("outside: a = %d\n", a);
return 0;
}
// pass_by_value.rs fn increment(mut x: i32) { x += 1; println!("inside: x = {}", x); } fn main() { let a = 10; increment(a); println!("outside: a = {}", a); }
Both print inside: 11, outside: 10. The function receives a copy.
For non-Copy types like String, Rust's pass-by-value transfers ownership:
// move_demo.rs fn take_string(s: String) { println!("got: {}", s); } fn main() { let msg = String::from("hello"); take_string(msg); // println!("{}", msg); // ERROR: value used after move }
Value flow (move):
main: msg ----[ownership transferred]----> take_string: s
msg is now invalid s is valid, then dropped
Parameter Passing: By Pointer (C)
To modify the caller's variable, C passes a pointer.
/* pass_by_pointer.c */
#include <stdio.h>
void increment(int *x)
{
*x = *x + 1;
}
int main(void)
{
int a = 10;
increment(&a);
printf("a = %d\n", a); /* 11 */
return 0;
}
Memory layout during the call:
main's stack frame increment's stack frame
+----------+ +----------+
| a = 10 | <------------ | x = &a |
+----------+ pointer +----------+
addr: 0x100 *x dereferences to 0x100
Caution: C does not prevent you from passing
NULL. Dereferencing a null pointer is undefined behavior and typically causes a segfault.
Parameter Passing: By Reference (Rust)
Rust uses references (& for shared, &mut for exclusive) instead of raw pointers.
// references.rs fn print_value(x: &i32) { println!("value = {}", x); } fn increment(x: &mut i32) { *x += 1; } fn main() { let mut a = 10; print_value(&a); increment(&mut a); println!("a = {}", a); // 11 }
Rust borrowing rules:
1. You can have MANY shared references (&T) at the same time
2. You can have ONE mutable reference (&mut T) at a time
3. You cannot have both at the same time
These rules are enforced at compile time.
Rust Note: References in Rust are always valid. They cannot be null, they cannot dangle, and the borrow checker ensures no data races. This is fundamentally safer than C pointers.
Try It: In Rust, try creating a
&mut awhile a&ais still in scope. Read the compiler error.
Multiple Return Values
C: returning a struct
/* multi_return_c.c */
#include <stdio.h>
typedef struct {
int quot;
int rem;
} divmod_result;
divmod_result divmod(int a, int b)
{
divmod_result r;
r.quot = a / b;
r.rem = a % b;
return r;
}
int main(void)
{
divmod_result r = divmod(17, 5);
printf("17 / 5 = %d remainder %d\n", r.quot, r.rem);
return 0;
}
Alternative: out-parameters via pointers.
/* out_params.c */
#include <stdio.h>
void divmod(int a, int b, int *quot, int *rem)
{
*quot = a / b;
*rem = a % b;
}
int main(void)
{
int q, r;
divmod(17, 5, &q, &r);
printf("17 / 5 = %d remainder %d\n", q, r);
return 0;
}
Rust: returning a tuple
// multi_return_rust.rs fn divmod(a: i32, b: i32) -> (i32, i32) { (a / b, a % b) } fn main() { let (quot, rem) = divmod(17, 5); println!("17 / 5 = {} remainder {}", quot, rem); }
Tuples are first-class. No struct or out-parameter boilerplate needed.
Try It: Write a function that returns
(min, max, sum)for a slice of integers in both C (using a struct) and Rust (using a tuple).
Function Pointers
C
/* fn_pointer.c */
#include <stdio.h>
int add(int a, int b) { return a + b; }
int mul(int a, int b) { return a * b; }
void apply(int (*op)(int, int), int x, int y)
{
printf("result = %d\n", op(x, y));
}
int main(void)
{
apply(add, 3, 4);
apply(mul, 3, 4);
return 0;
}
Rust
// fn_pointer.rs fn add(a: i32, b: i32) -> i32 { a + b } fn mul(a: i32, b: i32) -> i32 { a * b } fn apply(op: fn(i32, i32) -> i32, x: i32, y: i32) { println!("result = {}", op(x, y)); } fn main() { apply(add, 3, 4); apply(mul, 3, 4); }
Rust also supports closures that capture their environment:
// closures.rs fn apply(op: &dyn Fn(i32, i32) -> i32, x: i32, y: i32) { println!("result = {}", op(x, y)); } fn main() { let offset = 10; let add_with_offset = |a, b| a + b + offset; apply(&add_with_offset, 3, 4); // result = 17 }
Driver Prep: The Linux kernel makes heavy use of function pointers for abstraction. Every device driver fills in a struct of function pointers (
struct file_operations,struct net_device_ops). Understanding function pointers is essential for driver work.
Forward Declarations and Header Files (C)
In real C projects, declarations go in headers (.h), definitions in sources (.c).
/* math_ops.h */
#ifndef MATH_OPS_H
#define MATH_OPS_H
int add(int a, int b);
int mul(int a, int b);
#endif
/* math_ops.c */
#include "math_ops.h"
int add(int a, int b) { return a + b; }
int mul(int a, int b) { return a * b; }
/* main.c */
#include <stdio.h>
#include "math_ops.h"
int main(void)
{
printf("add: %d\n", add(3, 4));
printf("mul: %d\n", mul(3, 4));
return 0;
}
$ gcc -Wall -c math_ops.c -o math_ops.o
$ gcc -Wall -c main.c -o main.o
$ gcc math_ops.o main.o -o math_demo
$ ./math_demo
add: 7
mul: 12
C compilation flow (multi-file):
math_ops.h
|
v
math_ops.c ---[gcc -c]---> math_ops.o ---+
+--> [linker] --> math_demo
main.c -------[gcc -c]---> main.o -------+
^
|
math_ops.h (included)
Modules in Rust
Rust replaces header files with a module system.
#![allow(unused)] fn main() { // src/math_ops.rs pub fn add(a: i32, b: i32) -> i32 { a + b } pub fn mul(a: i32, b: i32) -> i32 { a * b } }
// src/main.rs mod math_ops; fn main() { println!("add: {}", math_ops::add(3, 4)); println!("mul: {}", math_ops::mul(3, 4)); }
No header files. No include guards. The pub keyword controls visibility.
Rust module system:
src/main.rs --[mod math_ops]--> src/math_ops.rs
pub fn add(...)
pub fn mul(...)
Rust Note: Rust's module system enforces encapsulation at compile time. Items without
pubare genuinely inaccessible from outside the module. In C, header files are documentation, not enforcement.
Static and Private Functions
C: static limits visibility to the current file
/* helpers.c */
static int helper(int x) { return x * 2; }
int public_function(int x) { return helper(x) + 1; }
Rust: omit pub
#![allow(unused)] fn main() { // helpers.rs fn helper(x: i32) -> i32 { x * 2 } pub fn public_function(x: i32) -> i32 { helper(x) + 1 } }
Functions are private by default in Rust. No keyword needed.
Recursion
/* factorial_c.c */
#include <stdio.h>
unsigned long factorial(unsigned int n)
{
if (n <= 1) return 1;
return n * factorial(n - 1);
}
int main(void)
{
for (unsigned int i = 0; i <= 10; i++) {
printf("%2u! = %lu\n", i, factorial(i));
}
return 0;
}
// factorial_rust.rs fn factorial(n: u64) -> u64 { if n <= 1 { 1 } else { n * factorial(n - 1) } } fn main() { for i in 0..=10 { println!("{:2}! = {}", i, factorial(i)); } }
Caution: Neither C nor Rust guarantees tail-call optimization. Deep recursion can overflow the stack. Prefer iterative solutions when depth is unbounded.
Quick Knowledge Check
- In C, what is the difference between a function declaration and a definition?
- What does
pubdo in Rust? - Why can you not use a
Stringafter passing it by value to a Rust function?
Common Pitfalls
- Forgetting the forward declaration in C. The compiler may assume
intreturn type, leading to subtle bugs. - Passing
NULLwhere a pointer is expected in C. No compile-time protection. Check forNULLdefensively. - Confusing
&and&mutin Rust. If you need to modify the argument, the function must take&mut T, and the caller must pass&mut value. - Forgetting that Rust strings are UTF-8. You cannot index a
Stringby byte position. Use.chars()for iteration. - Returning a pointer to a local variable in C. The stack frame is gone after return. The pointer dangles. Rust prevents this at compile time.
- Overusing
returnin Rust. Idiomatic style omitsreturnfor the last expression. Usereturnonly for early exits.