Skip to main content

Functions

Introduction

A function is a piece of code that can be executed repeatedly. It can accept different arguments and perform corresponding operations. The following example is a function.

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

The above code declares a function plus_one().

The syntax of the function declaration has the following points to note.

(1) Type of return value. When declaring a function, you first need to give the type of the return value. The above example is int, which means that the function plus_one() returns an integer.

(2) parameters. Function name after the parentheses inside, you need to declare the type of parameters and parameter names, plus_one(int n) that the function has an integer parameter n.

(3) function body. Function body to be written in curly brackets inside, after (that is, outside the curly brackets) do not need to add a semicolon. The starting position of the curly brackets, you can with the function name in the same line, you can also start another line, this book uses the same line of writing.

(4) return statement. return statement gives the return value of the function, the program runs to this line, it will jump out of the function body, the end of the function call. If the function does not return value, you can omit the return statement, or write as return; .

To call a function, simply put the function name followed by parentheses, with the actual arguments inside the parentheses, like the following.

int a = plus_one(13);
// a is equal to 14

When calling a function, the number of arguments must match the number of arguments inside the definition; too many or too few arguments will result in an error.

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

plus_one(2, 2); // error
plus_one(); // report an error

In the above example, the function plus_one() can only take one argument; passing in two arguments or none will result in an error.

Functions must be declared and then used, otherwise an error will be reported. This means that the function must be declared before plus_one() is used. If written like the following, an error will be reported at compilation.

int a = plus_one(13);

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

In the above example, the function is declared after the call to plus_one() and the compilation will report an error.

The C standard states that functions can only be declared at the top level of the source code file and not inside other functions.

Functions that do not return a value use the void keyword to indicate the type of return value. Functions with no arguments are declared with the void keyword to indicate the type of the argument.

void myFunc(void) {
// ...
}

The above myFunc() function has neither a return value nor is it called with any arguments.

The function can call itself, and this is called recursion. Here is an example of the Fibonacci sequence.

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

In the above example, the function Fibonacci() calls itself, simplifying the algorithm considerably.

main()

C specifies that main() is the entry function for a program, i.e. all programs must contain a main() function. The program always starts execution from this function, and without it the program cannot be started. All other functions are introduced into the program through it.

main() is written like any other function, giving the type of return value and the type of arguments, like the following.

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

In the example above, the final return 0; means that the function has finished running and returns 0.

The C convention is that a return value of 0 means that the function ran successfully; if it returns any other non-zero integer, it means that the run failed and there is something wrong with the code. The system determines whether a program has run successfully based on the return value of main(), which is used as the return value for the entire program.

Normally, if the return 0 line is omitted from main(), the compiler will automatically add it, i.e. the default return value of main() is 0. So, write it like this and it will work exactly the same.

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

Since C will only add return values by default to the main() function and will not do so for other functions, it is recommended to always keep the return statement for a consistent style of code.

Passing references for arguments

If the argument to a function is a variable, then when called, a copy of the value of that variable is passed in, not the variable itself.

void increment(int a) {
a++;
}

int i = 10;
increment(i);

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

In the above example, after calling increment(i), the variable i itself does not change and is still equal to 10. Because what is passed into the function is a copy of i, not i itself, the change in the copy does not affect the original variable. This is called a "pass-value reference".

So if the parameter variable changes, it is best to pass it out as the return value.

int increment(int a) {
a++;
return a;
}

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

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

Look at the following example again. The Swap() function is used to swap the values of two variables, and the following writeup will not work because of the pass-value reference.

void Swap(int x, int y) {
int temp;
temp = x;
x = y;
y = temp;
}

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

The above writeup will not have the effect of swapping variable values, because the variables passed in are copies of the original variables a and b, and no matter what the function does internally, it cannot affect the original variables.

If you want to pass in the variables themselves, there is only one way to do it, and that is to pass in the addresses of the variables.

void Swap(int* x, int* y) {
int temp;
temp = *x;
*x = *y;
*y = temp;
}

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

In the above example, by passing in the addresses of the variables x and y, the function can internally manipulate the address directly, thus swapping the values of the two variables.

Although not related to passing in references, it is specifically mentioned here that the function should not return pointers to internal variables.

int* f(void) {
int i;
// ...
return &i;
}

In the above example, the function returns a pointer to the internal variable i, which is written incorrectly. Because the internal variable disappears when the function finishes running, the memory address pointing to the internal variable i is then invalid, and it is dangerous to use it again.

Function pointers

A function itself is a piece of code inside memory, and C allows functions to be accessed via pointers.

void print(int a) {
printf("%d\n", a);
}

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

In the above example, the variable print_ptr is a function pointer to the address of the function print(). The address of the function print() can be obtained with &print. Note that (*print_ptr) must be written inside parentheses, otherwise the function argument (int) has higher priority than * and the whole equation becomes void* print_ptr(int).

With a function pointer, you can also call a function through it.

(*print_ptr)(10);
// is equivalent to
print(10);

More specifically, C also specifies that the function name itself is a pointer to the function code, and that the function address can be obtained by means of the function name. That is, print and &print are the same thing.

if (print == &print) // true

Thus, print_ptr in the above code is equivalent to print.

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

if (print_ptr == print) // true

So, for any function, there are five ways to write the call to the function.

// write method one
print(10)

// write two
(*print)(10)

// Write three
(&print)(10)

// Write method four
(*print_ptr)(10)

// write method 5
print_ptr(10)

For the sake of brevity and readability, function names are generally not preceded by * and &.

One application of this feature is that if a function has an argument or return value that is also a function, then the function prototype can be written as follows.

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

The above example makes it clear that the first argument of the function compute() is also a function.

Function prototypes

As mentioned earlier, functions must be declared first and used later. Since the program always runs the main() function first, this results in all other functions having to be declared before the main() function.

void func1(void) {
}

void func2(void) {
}

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

In the above code, the main() function must be declared at the end, otherwise the compiler will generate a warning and not find the declaration of func1() or func2().

However, main() is the entry point to the whole program and the main logic, so it is better to put it at the top. On the other hand, for programs with a large number of functions, it can become cumbersome to ensure that each function is in the correct order.

The solution provided by the C language is that functions can be used first and declared later, as long as the function prototype is given at the beginning of the program. By function prototype, we mean that the compiler is told in advance what type of return and type of argument each function will take. No other information is needed, and it is not necessary to include the function body; the specific function implementation can be added later.

int twice(int);

int main(int num) {
return twice(num);
}

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

In the above example, the implementation of the function twice() is placed after main(), but the prototype of the function is given first in the header of the code, so it can be compiled correctly. As long as the function prototype is given in advance, it doesn't matter where the exact implementation of the function is placed.

It is fine for the function prototype to include the parameter names, although this is redundant for the compiler, but it may help to understand the intent of the function when reading the code.

int twice(int);

// is equivalent to
int twice(int num);

In the example above, the argument name num of the twice function is allowed, whether or not it appears inside the prototype.

Note that the function prototype must end in a semicolon.

In general, the prototypes of all functions used by the current script are given in the header of each source code file.

exit()

The exit() function is used to terminate the entire program. Once this function is executed, the program will end immediately. The prototype of this function is defined in the header file stdlib.h.

exit() can return a value to the outside of the program, and its argument is the return value of the program. In general, two constants are used as its arguments: EXIT_SUCCESS (equivalent to 0) to indicate that the program ran successfully, and EXIT_FAILURE (equivalent to 1) to indicate that the program aborted abnormally. These two constants are also defined inside stdlib.h.

// The program ran successfully
// Equivalent to exit(0);
exit(EXIT_SUCCESS);

// program aborted with exception
// Equivalent to exit(1);
exit(EXIT_FAILURE);

Inside the main() function, exit() is equivalent to using the return statement. Other functions that use exit() are terminating the entire program and do nothing else.

The C language also provides an atexit() function to register additional functions to be executed when exit() is executed, to do some of the closing work when exiting the program. The prototype of this function is also defined in the header file stdlib.h.

``c int atexit(void (*func)(void));


The argument to `atexit()` is a function pointer. Note that its argument function (`print` in the example below) cannot take arguments and cannot have a return value.

```c
void print(void) {
printf("something wrong!\n");
}

atexit(print);
exit(EXIT_FAILURE);

In the above example, exit() will automatically call the print() function registered with atexit() before terminating the program.

Function descriptors

The C language provides a number of function descriptors to make function usage more explicit.

extern descriptors

For multi-file projects, the source code file will use functions declared in other files. In this case, inside the current file, you need to give the prototype of the external function and use extern to indicate that the function is defined from the other file.

extern int foo(int arg1, char arg2);

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

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

However, since the function prototype is extern by default, the effect is the same here without extern.

static descriptors

By default, each time a function is called, the function's internal variables are reinitialised and the values from the previous run are not retained. The static descriptor can change this behaviour.

When static is used inside a function to declare a variable, it means that the variable only needs to be initialised once and does not need to be initialised on each call. That is, its value remains unchanged between calls.

#include <stdio.h>

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

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

In the above example, the internal variable count of the function counter() is modified with the static descriptor, indicating that the variable is only initialised once and that each subsequent call will use the previous value, resulting in an incremental effect.

Note that static-modified variables can only be assigned to constants, not to variables, when they are initialised.

int i = 3;
static int j = i; // error

In the above example, j is a static variable and cannot be assigned to another variable, i, when initialised.

Also, in block scope, variables declared by static have the default value 0.

static int foo;
// Equivalent to
static int foo = 0;

static can be used to modify the function itself.

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

In the above example, the static keyword indicates that the function can only be used in the current file; without this keyword, other files can also use the function (by declaring the function prototype).

static can also be used inside parameters to modify the array of parameters.

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

In the above example, static will have no effect on the behaviour of the program, it is simply used to tell the compiler that the array is at least 3 in length, which can speed up the program in some cases. Also, note that for arguments to multi-dimensional arrays, static can only be used for the first dimension of the description.

const descriptors

The const descriptor inside a function parameter means that the parameter variable must not be modified inside the function.

void f(int* p) {
// ...
}

In the above example, the argument to the function f() is a pointer p, and the value it points to, *p, may be changed inside the function, thus affecting it outside.

To avoid this, you can declare the function with the const specifier in front of the pointer argument, telling the compiler that the value pointed to by that argument cannot be changed inside the function.

void f(const int* p) {
*p = 0; // the line reports an error
}

In the above example, when declaring the function, const specifies that the value pointed to by the pointer p cannot be modified, so *p = 0 would report an error.

But the way it is written above, only the value pointed to by p is restricted to be modified, while the address of p itself can be modified.

void f(const int* p) {
int x = 13;
p = &x; // modification is allowed
}

In the above example, p itself is modifiable, and const only restricts *p from being modified.

If you want to restrict the modification of p, you can put const in front of p.

void f(int* const p) {
int x = 13;
p = &x; // the line reports an error
}

If you want to restrict the modification of both p and *p, you need to use two consts.

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

Variable arguments

Some functions have an indeterminate number of arguments, and when declaring a function, you can use the ellipsis ... to indicate a variable number of arguments.

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

The above example is a prototype of the printf() function, where the number of arguments, other than the first one, is variable, related to the number of placeholders inside the format string. At this point, it is possible to use ... to indicate a variable number of arguments.

Note that the ... symbol must be placed at the end of the argument sequence, otherwise an error will be reported.

The header file stdarg.h defines a number of macros that can manipulate variable arguments.

(1) va_list: a data type to define a variable argument object. It must be used first when manipulating variable arguments.

(2) va_start: a function to initialise a variable parameter object. It accepts two parameters, the first parameter is the variable parameter object, the second parameter is the original function inside, the variable parameter before the parameter, used to position the variable parameter.

(3) va_arg: a function used to take out the current variable parameters, each call, the internal pointer will point to the next variable parameters. It accepts two arguments, the first is the variable argument object, the second is the type of the current variable argument.

(4) va_end: a function that is used to clean up the variable parameter object.

Here is an example.

double average(int i, ...) {
double total = 0;
va_list ap;
va_start(ap, i);
for (int j = 1; j <= i; ++j) {
total += va_arg(ap, double);
}
va_end(ap);
return total / i;
}

In the above example, va_list ap defines ap as a mutable argument object, va_start(ap, i) puts the arguments after argument i into ap uniformly, va_arg(ap, double) is used to take one argument from ap in turn and specify that the argument is of type double, and va_end(ap) is used to clean up variable argument objects.