Overview

A function is a reusable block of code that can accept different parameters and perform specific tasks. Here’s an example of a function:

1
2
3
int plus_one(int n) {
return n + 1;
}

This code declares a function named plus_one().

Key Points of Function Declarations:

  1. Return Type: Specify the return type at the start of the declaration. In the example, int indicates the function returns an integer.
  2. Parameters: Declare the types and names of parameters in parentheses following the function name. plus_one(int n) means the function takes one integer parameter, n.
  3. Function Body: The body of the function is enclosed in braces {}. No semicolon is needed after the closing brace. Braces can start on the same line or a new line; this book uses the same line.
  4. Return Statement: The return statement specifies the function’s return value and exits the function. If the function does not return a value, you can omit the return statement or use return;.

To call a function, write its name followed by parentheses containing the actual arguments:

1
2
int a = plus_one(13);
// a is 14

The number of arguments in the function call must match the function’s declaration. Too many or too few arguments will cause an error.

1
2
3
4
5
6
int plus_one(int n) {
return n + 1;
}

plus_one(2, 2); // Error
plus_one(); // Error

In the above example, plus_one() only accepts one argument. Passing two or no arguments will cause an error.

A function must be declared before it is used:

1
2
3
4
5
int a = plus_one(13);

int plus_one(int n) {
return n + 1;
}

In the above example, the function plus_one() is used before it is declared, leading to a compilation error.

The C standard specifies that functions must be declared at the top level of the source file and cannot be declared inside other functions.

For functions that do not return a value, use the void keyword:

1
2
3
void myFunc(void) {
// ...
}

The myFunc() function neither returns a value nor requires parameters.

Functions can call themselves, a technique known as recursion. Here is an example with the Fibonacci sequence:

1
2
3
4
5
6
unsigned long Fibonacci(unsigned n) {
if (n > 2)
return Fibonacci(n - 1) + Fibonacci(n - 2);
else
return 1;
}

In this example, Fibonacci() calls itself, simplifying the algorithm.

main() Function

In C, main() is the entry point of the program and is required for every program. Execution starts here. Other functions are invoked through main().

The main() function is declared like other functions, specifying the return type and parameters:

1
2
3
4
int main(void) {
printf("Hello World\n");
return 0;
}

The return 0; statement indicates the function ends successfully. A return value of 0 signifies success, while a non-zero value indicates an error. The return value of main() determines the program’s success.

If return 0; is omitted, the compiler adds it by default, making the following equivalent:

1
2
3
int main(void) {
printf("Hello World\n");
}

Since the compiler only defaults the return value for main(), it’s recommended to always include the return statement for consistency.

Passing Parameters by Value

When a function parameter is a variable, the value passed into the function is a copy of that variable, not the variable itself.

1
2
3
4
5
6
7
8
void increment(int a) {
a++;
}

int i = 10;
increment(i);

printf("%d\n", i); // 10

In this example, after calling increment(i), the variable i remains unchanged, still equal to 10. This is because what is passed to the function is a copy of i, not i itself. Changes to the copy do not affect the original variable. This is known as “pass by value.”

To modify the variable, you should return the new value:

1
2
3
4
5
6
7
8
9
int increment(int a) {
a++;
return a;
}

int i = 10;
i = increment(i);

printf("%d\n", i); // 11

In this revised example, the function increment returns the incremented value, and i is updated with the returned value.

Swapping Values

Consider the Swap() function that aims to swap two variables’ values. Due to pass-by-value, the following approach won’t work:

1
2
3
4
5
6
7
8
9
10
void Swap(int x, int y) {
int temp;
temp = x;
x = y;
y = temp;
}

int a = 1;
int b = 2;
Swap(a, b); // Ineffective

This code does not swap the values of a and b because the function receives copies of a and b. Changes within the function do not affect the original variables.

To modify the original variables, pass their addresses instead:

1
2
3
4
5
6
7
8
9
10
void Swap(int* x, int* y) {
int temp;
temp = *x;
*x = *y;
*y = temp;
}

int a = 1;
int b = 2;
Swap(&a, &b);

By passing the addresses of a and b, the function can directly modify the original variables.

Note that functions should not return pointers to internal variables.

1
2
3
4
5
int* f(void) {
int i;
// ...
return &i;
}

In the example above, the function returns a pointer to the internal variable i, which is incorrect. When the function finishes execution, the internal variable disappears, making the pointer to i invalid. Using this address afterward is very dangerous.

Function Pointers

Returning Function Pointers

Functions are essentially blocks of code stored in memory, and C allows functions to be accessed through pointers.

1
2
3
4
5
void print(int a) {
printf("%d\n", a);
}

void (*print_ptr)(int) = &print;

Here, print_ptr is a function pointer that points to the print function. You can use &print to get the function’s address, but print alone is sufficient.

You can call the function through the pointer like this:

1
2
3
(*print_ptr)(10);
// Equivalent to
print(10);

In C, the function name itself acts as a pointer to the function, so print and &print are equivalent.

1
if (print == &print) // true

Therefore, print_ptr is effectively the same as print:

1
2
3
4
5
void (*print_ptr)(int) = &print;
// or
void (*print_ptr)(int) = print;

if (print_ptr == print) // true

You can call a function using any of these five methods:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Method 1
print(10)

// Method 2
(*print)(10)

// Method 3
(&print)(10)

// Method 4
(*print_ptr)(10)

// Method 5
print_ptr(10)

Typically, function names are used without * and & for simplicity.

Function Pointers as Parameters

A function’s parameters or return values can also be functions. This is how you declare such functions:

1
int compute(int (*myfunc)(int), int, int);

This prototype indicates that compute()‘s first parameter is a function.

Function Prototypes

As mentioned earlier, functions must be declared before they are used. Since the main() function is executed first in a program, all other functions must be declared before it.

Here is an example:

1
2
3
4
5
6
7
8
9
10
11
void func1(void) {
}

void func2(void) {
}

int main(void) {
func1();
func2();
return 0;
}

In the code above, the main() function must be declared last. If not, the compiler will issue a warning because it won’t find the declarations for func1() or func2().

However, since main() is the entry point of the program and contains the main logic, it is often better to place it at the beginning. On the other hand, with many functions, ensuring the correct order can become cumbersome.

To address this, C provides a solution: you can declare function prototypes at the beginning of the program, allowing you to use the functions before defining them. A function prototype tells the compiler the return type and parameter types of the function in advance. You don’t need to include the function body; the actual implementation can come later.

Here’s an example:

1
2
3
4
5
6
7
8
9
10
int twice(int);

int main(void) {
int num = 5;
return twice(num);
}

int twice(int num) {
return 2 * num;
}

In this example, the function twice() is defined after main(), but because the prototype is provided at the top, the code compiles correctly. As long as you provide the prototype in advance, the placement of the function definition is flexible.

Function prototypes may also include parameter names, which can help readers understand the function’s purpose, although the compiler does not require them:

1
2
3
4
int twice(int);

// Equivalent to
int twice(int num);

In this example, whether or not the parameter name num appears in the prototype is optional.

Note that function prototypes must end with a semicolon.

Typically, the top of each source file contains the prototypes for all the functions used in that file.

exit()

The exit() function is used to terminate a program immediately. Once this function is called, the program ends right away. The prototype of exit() is defined in the header file stdlib.h.

The exit() function can return a value to the operating system. The argument passed to exit() represents the program’s return status. Generally, two constants are used as arguments: EXIT_SUCCESS (equivalent to 0) indicates successful execution, while EXIT_FAILURE (equivalent to 1) indicates an abnormal termination. Both constants are defined in stdlib.h.

1
2
3
4
5
6
7
// Successful execution
// Equivalent to exit(0);
exit(EXIT_SUCCESS);

// Abnormal termination
// Equivalent to exit(1);
exit(EXIT_FAILURE);

In the main() function, exit() is equivalent to using the return statement. When used in other functions, exit() terminates the entire program and has no other effects.

C also provides the atexit() function, which registers additional functions to be executed when exit() is called. This can be used for cleanup tasks when the program terminates. The prototype of atexit() is also defined in stdlib.h.

1
int atexit(void (*func)(void));

The parameter to atexit() is a function pointer. Note that the function pointed to (in the example, print) must not take parameters and must not return a value.

1
2
3
4
5
6
void print(void) {
printf("something went wrong!\n");
}

atexit(print);
exit(EXIT_FAILURE);

In the example above, when exit() is called, the print() function registered with atexit() will be executed first, followed by the termination of the program.

Function Specifiers in C

C provides several function specifiers that help clarify how functions are used.

extern Specifier

In multi-file projects, source files often use functions declared in other files. In such cases, the current file needs to provide the function prototype and use the extern specifier to indicate that the function definition is in another file.

1
2
3
4
5
6
7
extern int foo(int arg1, char arg2);

int main(void) {
int a = foo(2, 3);
// ...
return 0;
}

In this example, foo() is defined in another file, and extern tells the compiler that this file does not contain the definition of foo().

However, because function prototypes are extern by default, including extern is optional and does not change the behavior.

static Specifier

By default, each time a function is called, its internal variables are reinitialized, and their previous values are not retained. The static specifier changes this behavior.

When used inside a function to declare a variable, static ensures that the variable is initialized only once and retains its value between function calls.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

void counter(void) {
static int count = 1; // Initialized only once
printf("%d\n", count);
count++;
}

int main(void) {
counter(); // 1
counter(); // 2
counter(); // 3
counter(); // 4
}

In this example, the count variable in the counter() function is declared with static, meaning it is initialized only once and retains its value across calls, resulting in an incrementing effect.

Note that static variables can only be initialized with constants, not variables.

1
2
int i = 3;
static int j = i; // Error

In the above example, j is a static variable, and it cannot be initialized with another variable i.

Additionally, within block scope, static variables have a default value of 0.

1
2
3
static int foo;
// Equivalent to
static int foo = 0;

The static specifier can also be used to modify functions themselves.

1
2
3
4
static int Twice(int num) {
int result = num * 2;
return result;
}

Here, static means that the function Twice() is only accessible within the current file. Without static, other files could also use this function (by declaring its prototype).

static can also be used with parameters to specify array sizes.

1
2
3
int sum_array(int a[static 3], int n) {
// ...
}

In this case, static indicates to the compiler that the array must have at least 3 elements. This information can sometimes improve performance. Note that static can only specify the size of the first dimension in multi-dimensional arrays.

const Specifier

The const specifier in function parameters indicates that the function should not modify the variable pointed to by the parameter.

1
2
3
void f(int* p) {
// ...
}

Here, f() takes a pointer p, which can modify the value pointed to by p, affecting the outside world.

To prevent this modification, you can use const to indicate that the function cannot alter the value pointed to by the pointer.

1
2
3
void f(const int* p) {
*p = 0; // Error
}

In this example, const specifies that the value pointed to by p cannot be modified, so *p = 0 will produce an error.

However, the address stored in p itself can still be changed.

1
2
3
4
void f(const int* p) {
int x = 13;
p = &x; // Allowed
}

In this case, modifying p is allowed, but modifying *p is not.

If you want to restrict modifications to p, place the const keyword before p.

1
2
3
4
void f(int* const p) {
int x = 13;
p = &x; // This line will cause an error
}

To prevent modification of both p and the value it points to, use const in both places.

1
2
3
void f(const int* const p) {
// ...
}

In this declaration, const applies to both the pointer p and the value it points to.

Variable Arguments

Some functions have an indeterminate number of parameters. When declaring such functions, you can use ellipses (...) to indicate a variable number of arguments.

For example, the printf() function is declared as:

1
int printf(const char* format, ...);

In this prototype, the number of arguments following the first one is variable and depends on the format string’s placeholders. The ellipses (...) indicate this variability.

Note that the ellipses must appear at the end of the parameter list; otherwise, it will cause an error.

The header file stdarg.h defines macros for handling variable arguments:

  1. va_list: A type used to define a variable argument object. It must be initialized before accessing variable arguments.
  2. va_start: A function that initializes the variable argument object. It takes two parameters: the variable argument object and the last fixed parameter before the variable arguments to locate them.
  3. va_arg: A function used to retrieve the current variable argument. After each call, it advances the internal pointer to the next argument. It takes two parameters: the variable argument object and the type of the current argument.
  4. va_end: A function that cleans up the variable argument object.

Here’s an example:

1
2
3
4
5
6
7
8
9
10
double average(int count, ...) {
double total = 0;
va_list ap;
va_start(ap, count);
for (int i = 0; i < count; ++i) {
total += va_arg(ap, double);
}
va_end(ap);
return total / count;
}

In this example, va_list ap defines ap as the variable argument object. va_start(ap, count) initializes ap to access the arguments following count. va_arg(ap, double) retrieves each argument as a double, and va_end(ap) cleans up the variable argument object.