Functions in C Language
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 | int plus_one(int n) { |
This code declares a function named plus_one()
.
Key Points of Function Declarations:
- Return Type: Specify the return type at the start of the declaration. In the example,
int
indicates the function returns an integer. - 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
. - 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. - 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 thereturn
statement or usereturn;
.
To call a function, write its name followed by parentheses containing the actual arguments:
1 | int a = plus_one(13); |
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 | int plus_one(int n) { |
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 | int a = plus_one(13); |
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 | 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 | unsigned long Fibonacci(unsigned n) { |
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 | int main(void) { |
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 | int main(void) { |
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 | void increment(int a) { |
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 | int increment(int a) { |
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 | void Swap(int x, int y) { |
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 | void Swap(int* x, int* y) { |
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 | int* f(void) { |
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 | void print(int a) { |
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 | (*print_ptr)(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 | void (*print_ptr)(int) = &print; |
You can call a function using any of these five methods:
1 | // Method 1 |
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 | void func1(void) { |
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 | int twice(int); |
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 | int twice(int); |
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 | // Successful execution |
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 | void print(void) { |
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 | extern int foo(int arg1, char arg2); |
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 |
|
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 | int i = 3; |
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 | static int foo; |
The static
specifier can also be used to modify functions themselves.
1 | static int Twice(int num) { |
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 | 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 | 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 | void f(const int* p) { |
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 | void f(const int* p) { |
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 | void f(int* const p) { |
To prevent modification of both p
and the value it points to, use const
in both places.
1 | 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:
- va_list: A type used to define a variable argument object. It must be initialized before accessing variable arguments.
- 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.
- 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.
- va_end: A function that cleans up the variable argument object.
Here’s an example:
1 | double average(int 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.