Functions ๐ program structure
Function definitions, declarations and calls
A function must be declared or defined before it is called.
(in early C versions existed implicit declaration).
Function declaration (prototype)
informs the compiler that the function exists
can be repeated multiple times
allows calling a function before its definition (so-called forward declaration)
usually placed in header files (
.h)
Function definition
contains the complete implementation of the function, including its body
may appear only once in the program, otherwise a compile or link error occurs
usually placed in "source files" (
.c)
Function call
creates a call frame on the stack
evaluates the arguments and stores them on the stack (or in registers)
transfers control to the function body (machine instruction
CALL)on completion returns (
RET) the function value (if the function is notvoid) and discards the call frame
Binary calling convention:
In C:
arguments are pushed on the stack in reverse order (right to left)
the frame is restored by the caller
In Pascal:
arguments are pushed on the stack in order (left to right)
the frame is restored by the callee
Compiler-specific directives for calling conventions (GCC):
cint __attribute__((cdecl)) f(int a, int b); // caller cleans up the stack (C default)
int __attribute__((stdcall)) g(int a, int b); // callee cleans up (Pascal-like, Win32 API)
int __attribute__((fastcall)) h(int a, int b); // first args in ECX/EDX
int __attribute__((regparm(3))) k(int a, int b, int c); // first 3 args in EAX/EDX/ECX
int __attribute__((ms_abi)) w(int a); // x86-64: use the Microsoft ABI
int __attribute__((sysv_abi)) s(int a); // x86-64: use the System V ABI
BNF
bnf<function-declaration> ::= <declaration-specifiers> <declarator> ";"
<declaration-specifiers> ::= <type-specifier> | <type-qualifier> <declaration-specifiers>
<type-specifier> ::= "void" | "char" | "int" | "float" | "double" | <typedef-name>
<declarator> ::= <direct-declarator>
<direct-declarator> ::= <identifier> "(" <parameter-type-list>? ")"
<parameter-type-list> ::= <parameter-list> ["," "..."]
<parameter-list> ::= <parameter-declaration> | <parameter-list> "," <parameter-declaration>
<parameter-declaration> ::= <declaration-specifiers> <declarator>
bnf<function-definition> ::= <declaration-specifiers> <declarator> <compound-statement>
<compound-statement> ::= "{" <block-item-list>? "}"
<block-item-list> ::= <block-item> | <block-item-list> <block-item>
<block-item> ::= <declaration> | <statement>
<statement> ::= <labeled-statement> | <expression-statement>
| <selection-statement> | <iteration-statement>
| <jump-statement>
bnf<function-call> ::= <postfix-expression> "(" <argument-expression-list>? ")"
<postfix-expression> ::= <identifier>
| <postfix-expression> "[" <expression> "]"
| <postfix-expression> "." <identifier>
<argument-expression-list> ::= <assignment-expression>
| <argument-expression-list> "," <assignment-expression>
Parameters, arguments and return values
parameters: formal variables in the function definition
arguments: actual values passed at the call time
Passing arguments by value (pass by value):
a copy of the argument is created
changes inside the function do not affect the original variable
the standard way in C
Example:
cvoid inc(int x) {
x++;
}
int a = 5;
inc(a);
// a is still 5
Passing by pointer (pass by pointer/reference):
allows modifying the original variable
the _address_ of the variable is passed
Example:
cvoid inc(int *x) {
(*x)++;
}
int a = 5;
inc(&a); // we pass the address of a
// a is now 6
return and return values
The
returnkeyword terminates the function and returns a valueThe type of the return value must match the declaration
A
voidfunction does not need areturn, or it may havereturn;without a value
Example:
cint add(int a, int b) {
return a + b;
}
void say_hello() {
printf("Hello, World!\n");
}
Early return
A function may contain multiple
returnstatements
Example:
cint max(int a, int b) {
if (a > b)
return a;
else
return b;
}
With early return, simplified:
cint max(int a, int b) {
if (a > b) return a;
return b;
}
Variable scope and lifetime
Local variables ("automatic")
declared inside a function or block
visible only in the block where they are declared
exist only while the block is executing
stored on the stack
each call (sequential or recursive) of a function has its own copy
without initialization they contain undefined values
Global variables
declared outside functions
visible in all functions from the point of declaration onward
exist for the entire run of the program
automatically initialized to 0 (by the linker/loader)
stored in the data section of memory
use them carefully and sparingly (in C++ the order of initialization is problematic)
External variables
global, declared with the
externkeyworda declaration without a definition (does not allocate memory)
allow sharing global variables between multiple files
exactly one file must contain the definition without
extern
Static variables
local static:
static int x = 0;visibility: inside the function
lifetime: the entire run of the program
retain their value between calls
initialized only once, at first initialization
global static:
visibility limited to the current translation unit = the
.cfilestored in the data section of memory
Register variables
automatic variables declared with the
registerkeyworda hint to store the variable in a register (not guaranteed)
have no address,
&cannot be usedtoday is the optimization left to the compiler
Dynamic variables
allocated using
malloc(),calloc(),realloc()exist from allocation until release (
free())stored on the heap
must be released explicitly, otherwise a memory leak occurs
Summary
Scope/visibility of variables
block: the variable is valid only within the block
{ }function: function parameters and local variables
file, program: static and global variables
Lifetime
automatic: from declaration to the end of the block
static: the entire run of the program
dynamic: from
malloc()tofree()
Modular program structure
splitting a program into multiple files / modules / translation units
each module can have its own interface
header file (.h): declarations of global types, functions, global variables
implementation file (.c): definitions of local types, functions and variables (static)
MakefileCC = cc
CFLAGS = -std=c23 -Wall -Wextra # -g
# Targets that are not files; always run when requested
.PHONY: all run clean
# Default target (plain `make`): build the executable
all: main
# Link step: combine object files into the executable `main`
# $@ = target name (main), $^ = all prerequisites (main.o funs.o scope.o)
main: main.o funs.o scope.o
$(CC) $(CFLAGS) -o $@ $^
# Pattern rule: build any X.o from X.c
# Depends on X.h too, so editing a header triggers recompilation
# (note: main.h does not exist, %.h is replaced by space)
# -c = compile only, no linking; $< = first prerequisite (X.c)
%.o: %.c %.h
$(CC) $(CFLAGS) -c -o $@ $<
# Build, then run the program
run: all
./main
# Remove build artifacts (-f: no error if files don't exist)
clean:
rm -f main *.o a.out
main.c#include <stdio.h>
#include "funs.h"
#include "scope.h"
static void header(const char* title) {
// OR: static void header(char const* title) {
printf("\n=== %s ===\n", title);
}
// ends with semicolon: "function-like" macro
#define HEADER(title) header(title);
#define PRINT_1_(expr) printf("%s = %d\n", #expr, (int) expr);
#define PRINT_2_(prefix, expr) \
printf("%s: %s = %d\n", prefix, #expr, (int) expr);
int main(void) {
HEADER("Functions: declare, define, call")
PRINT_1_(add(2, 3))
PRINT_1_(max(7, 4))
HEADER("Argument passing")
int a = 5;
PRINT_1_(a);
inc_by_value(a);
PRINT_2_("after inc_by_value(a)", a);
inc_by_pointer(&a);
PRINT_2_("after inc_by_pointer(&a)", a);
HEADER("Recursion")
for (int i = 0; i <= 5; ++i) {
printf("factorial(%d) = %d\n", i, factorial(i));
}
HEADER("Dynamic variables (malloc/free)")
int values[] = {2, 4, 6, 8};
PRINT_1_(avg_dynamic(values, 4));
HEADER("Scope, lifetime, extern, static")
PRINT_2_("start", global_counter);
set_global_counter(10);
inc_global_counter();
PRINT_2_("end", global_counter);
PRINT_2_("start", get_file_private_state());
set_file_private_state(250);
PRINT_2_("end", get_file_private_state());
PRINT_1_(call_count_demo());
PRINT_1_(call_count_demo());
PRINT_1_(call_count_demo());
HEADER("Structs and typedefs")
Point p1 = create_point(2, 3);
Point p2 = create_point(4, 5);
Point p3 = add_points(p1, p2);
printf("p3 = (%d, %d)\n", p3.x, p3.y);
// HEADER("Out of bounds stack")
// for (int i = 2;; i *= 2) {
// printf("factorial(%d) = %d\n", i, factorial(i));
// }
return 0;
}
funs.h#pragma once
int add(int a, int b);
int max(int a, int b);
void inc_by_value(int x);
void inc_by_pointer(int* x);
int factorial(int n);
double avg_dynamic(const int* values, int n);
typedef struct {
int x;
int y;
} Point;
Point create_point(int x, int y);
Point add_points(Point p1, Point p2);
funs.c#include "funs.h"
#include <stdlib.h>
static int sum_array(const int* values, int n);
int add(int a, int b) {
return a + b;
}
int max(int a, int b) {
if (a > b)
return a;
return b;
}
void inc_by_value(int x) {
x++;
(void) x;
}
void inc_by_pointer(int* x) {
if (x)
(*x)++;
}
int factorial(int n) {
if (n <= 1)
return 1;
return n * factorial(n - 1);
}
double avg_dynamic(const int* values, int n) {
if (!values || n <= 0)
return 0.0;
// not needed, only to demonstrate dynamic memory allocation
int* copy = (int*) malloc((size_t) n * sizeof(int));
if (!copy)
return 0.0;
for (int i = 0; i < n; ++i) {
copy[i] = values[i];
}
int sum = sum_array(copy, n);
// free the dynamically allocated memory
// comment out to demonstrate memory leak
free(copy);
return (double) sum / (double) n;
}
static int sum_array(const int* values, int n) {
int sum = 0;
for (int i = 0; i < n; ++i) {
sum += values[i];
}
return sum;
}
Point create_point(int x, int y) {
Point p = {x, y};
return p;
}
Point add_points(Point p1, Point p2) {
Point result = {p1.x + p2.x, p1.y + p2.y};
return result;
// return (Point){p1.x + p2.x, p1.y + p2.y, 9};
}
/* >>> */
static int file_private_state = 100;
__attribute__((unused)) static int get_file_private_state(void) {
return file_private_state;
}
__attribute__((unused)) static void set_file_private_state(int value) {
file_private_state = value;
}
scope.h#pragma once
extern int global_counter; /* >>> */
void set_global_counter(int value);
void inc_global_counter(void);
int get_file_private_state(void);
void set_file_private_state(int value);
int call_count_demo(void);
scope.c#include "scope.h"
int global_counter = 0;
static int file_private_state = 100;
void set_global_counter(int value) {
global_counter = value;
}
// note: (void) is required in pre-C23, but optional (ignored) in C23
void inc_global_counter(void) {
global_counter++;
}
int get_file_private_state(void) {
return file_private_state;
}
void set_file_private_state(int value) {
file_private_state = value;
}
int call_count_demo(void) {
static int calls = 0;
calls++;
return calls;
}
Recursion
a function calls itself
should have a termination condition
each recursive call has its own stack frame with copies of the local variables
Example:
cint fact(int n) {
if (n <= 1) return 1; // termination condition
return n * fact(n - 1); // recursive call
}
Types of recursion
direct: a function calls itself directly
indirect: e.g. function A calls B, which calls A
tail recursion: the recursive call is the last operation in the function, which enables optimization (tail call optimization), though not guaranteed by the C standard