Functions in C¶
Learning Objectives: The student knows the structure of a C program and can use functions in a C program.
As we have already hinted in the introductory material, every C program always has at least one function, called
main
. This function is automatically called by the operating system (or firmware) when the program starts. In fact, the execution of a C program lasts exactly as long as the execution of the main function. But more about this in the hardware-oriented section...In the course, we think of a C program modularly as a collection of different independent functionalities. Each functionality/task has its own inputs/parameters, functionality, and response/results. In modular programming, tasks are implemented as separate modules / subroutines, which in C are called functions. Thus, the logic of the program is based on passing the results and arguments of functions as variables from one function to another. For example, reading measurement results from a temperature sensor in an embedded system:
- Initialize and calibrate the temperature sensor
// Function to initialize the temperature sensor void initialize_sensor() { // Code to set up I2C communication setup_I2C(); // Code to configure the sensor's registers configure_sensor_registers(); } // Function to set up I2C communication void setup_I2C() { // Hardware-specific code to initialize I2C communication // Example: Set the clock speed, enable the I2C peripheral, etc. } // Function to configure the sensor's registers void configure_sensor_registers() { // Hardware-specific code to configure the sensor // Example: Set the resolution, measurement mode, etc. }
- Request the measurement result from the sensor
// Function to read the temperature from the sensor float read_temperature() { // Send command to the sensor to start a measurement send_measurement_command(); // Read the data from the sensor's output register int raw_data = read_sensor_data(); // Convert the raw data to a temperature value float temperature = convert_to_temperature(raw_data); return temperature; } // Function to send a measurement command to the sensor void send_measurement_command() { // Hardware-specific code to send a command to the sensor // Example: Write to a specific register address via I2C } // Function to read data from the sensor int read_sensor_data() { // Hardware-specific code to read data from the sensor's output register // Example: Read bytes from a specific register address via I2C int data = 0; // Placeholder for actual data read return data; } // Function to convert raw sensor data to a temperature value float convert_to_temperature(int raw_data) { // Code to convert the raw data to a human-readable temperature value // Example: Apply the sensor-specific formula float temperature = raw_data * 0.01; // Example conversion factor return temperature; }
- Display the measurement result on the screen
// Function to display the temperature on the screen void display_temperature(float temperature) { // Code to format and send the temperature value to the display // Example: Send data to an LCD or a serial terminal printf("Current Temperature: %.2f°C\n", temperature); }
The
main
function usually acts as an entry point of the C program, but the implementation of the different functionality is done in other functions that are called from main
. int main() { // Initialize the temperature sensor initialize_sensor(); // Read the temperature float temperature = read_temperature(); // Display the temperature display_temperature(temperature); return 0; }
Note! In Python the equivalent to main would be:
def main():
# Main program logic goes here
print("This is the main function.")
if __name__ == "__main__":
main()
Note! All C programming exercises in the course will involve implementing different functions.
This way of programming offers multiple advantages, among them:
- Encapsulation: Each function encapsulates specific part of the hardware interaction. The purpose of encapsulation is to protect the internal state of task/object to ensure that data can only be modified in a controlled way.
- Modularity: Program is broken down into smaller and more manageable and independent module, each module representing a functionality. The purpose is to make the program easy to understand and maintain.
- Reusability: Functions can be reused in other part of the program. No need to write the function again.
First C Program¶
Let's first take a general look at the structure of a C program. At this point, you don't need to know everything that happens in the code below, but rather to grasp the four essential parts of a C program's structure.
/**************************************
* First C program
**************************************/
/*
* Preprocessor directives
*/
// Include the stdio library in the program
#include <stdio.h>
// Define the constant PI
#define PI 3.14159
/*
* Declaration of internal functions and variables in the program
*/
// Declare a custom function calculate_area using a prototype
float calculate_area(float radius);
/*
* Implementation of the main function
*/
int main() {
// Declaration (and initialization) of internal variables in the main program
float circle_area = 0.0, circle_radius = 0.0;
// Main program functionality
printf("Enter the circle's radius: ");
scanf("%f", &circle_radius);
circle_area = calculate_area(circle_radius);
printf("The area of the circle is: %.2f\n", circle_area);
// Return value of the main program (function)
return 0;
}
/*
* Function: calculate_area, calculates the area of a circle given its radius
* Arguments: radius, the circle's radius
*/
float calculate_area(float radius) {
//Declaration (and initialization) of internal variables in the function
float area = 0.0;
// Functionality of the function
area = PI * radius * radius;
// Return value of the function
return area;
}
Let's examine the components that make up a C program:
1. Preprocessor directives guide the compilation process of the program and serve various purposes. At this stage, you need to know:
1. Preprocessor directives guide the compilation process of the program and serve various purposes. At this stage, you need to know:
- You can include your own or pre-existing libraries from the compiler environment in a C program.
- In the example, the C library stdio is used, which provides functions for handling input and output.
- Constants and macros can be defined in programs.
- In the example, a constant PI was created for use in the program.
2. Declaration of internal functions and variables in the program using prototypes. The prototype tells the program what functions are available.
- The example prototype
float calculate_area(float radius);
indicates that a function named calculate_area is available and that it takes and returns floating-point numbers. - In fact, when we include libraries in a C program, often only the function prototypes, constants, etc., are included. More on this shortly...
3. Implementation of the main function
main
.- In the example, the main function asks the user for the radius of a circle and calculates its area using the calculate_area function.
4. The functionality of the internal functions declared in the program.
Function Prototype¶
In C programming, a function is introduced through its prototype. A prototype generally takes the following form:
return_type
, the type of value that the function returns at the end of its execution.function_name
, the name by which the function is called in the program.( variable_type1 variable1, variable_type2 variable2, ...)
, these definitions indicate what types of parameters the function accepts and by what variable names they can be handled within the function.
Example. A function named
calculate_area
returns a floating-point number of type float and takes a floating-point number as input (in the variable radius).// Declare a custom function calculate_area using a prototype
float calculate_area(float radius);
Why a Prototype?¶
Okay, but why do we need a prototype? Without delving too deep into the world of compilers, let's just say that in C, a prototype allows a function to be used in a program before its code has been compiled. In the example, the
main
function can already use the calculate_area
function before its code has been encountered by the compiler as it processes the C source file.The compiler thus trusts the programmer's promise that an implementation of the function matching the prototype will appear at some point during the compilation process. Upon finding the prototype, the compiler only checks the syntax of the function call, ensuring that the function is called correctly in terms of name and parameters.
From now on in this course, when we create functions, we will always first declare a prototype! If the implementation is not found in the program's source files or included libraries, the compilation will end with an error message. Well-behaved compilers will even halt the entire compilation process and warn you if the prototype is missing from the program.
In the code example above:
// The prototype promises that such a function exists
float calculate_area(float radius);
// With the prototype, the function can be used
int main() {
circle_area = calculate_area(circle_radius);
}
// The actual function implementation
float calculate_area(float radius) {
float area = 0.0;
area = PI * radius * radius;
return area;
}
Let's revisit the preprocessor directives briefly: in fact, the command
#include <stdio.h>
fetches the prototypes for the scanf
and printf
functions that we use in our program. This function is in stdio.h. This allows us to bring ready-made library functions into our program via their prototypes.Note! C programming exercises also require a prototype in the task solution.
Function Parameters¶
Function parameters, written between parenthesis, defines which values should receive a function. A parameter, and the value passed to the function, i.e., the argument, is a way to pass information to the function from the calling code and to return information from the function to the calling code.
- Parameters can be any of the standard C variable types
int, long, double, float, char, struct
as well as derived types and arrays. - If there are multiple parameters, they are separated by commas.
- A function can accept parameters of different types.
In the example code above, the function is declared to take a single floating-point argument.
float calculate_area(float radius);
If a function does not take arguments or does not return a value, the reserved word
void
is used in its declaration for clarity.void not_returning_anything(uint8_t x, uint8_t y);
int32_t no_arguments_needed(void);
void leave_me_alone(void);
Technically, the word
void
is not mandatory in such function declarations, but without void
, the compiler might assume certain things about the function... for example, without void
, the compiler might assume the function returns an int type integer, which could lead to warnings or error messages. For clarity, it is always advisable to use the word void
when necessary.Note! In the example, the
main
function is implemented in the program without parameter definitions. In some development environments, you will encounter main functions with defined parameters. These parameters include command line arguments provided when launching the program. This is common on workstations but not typically used in embedded systems, which is why you won't see it in course examples.Function Implementation¶
The function implementation is done modularly within its own code block, which in C is marked and enclosed with curly braces as a separate functional unit. (In Python, curly braces are replaced by indentation.)
/*
* Function: calculate_area, calculates the area of a circle given its radius
* Arguments: radius, the circle's radius
*/
float calculate_area(float radius) {
// Declaration (and initialization) of internal variables in the function
float area = 0.0;
// Function functionality
area = PI * radius * radius;
// Function return value
return area;
}
The function implementation is also divided into three parts:
- Functions usually declare internal, i.e., local, variables.
- Local variables exist only within the function during its execution and disappear when the execution ends.
- In the example above, the variable
area
is a local variable. - The current C standard does allow for variable declarations in the middle of the code, but it is still clearest for a beginner to declare variables collectively at the beginning of the function implementation.
- The function implementation consists of C language statements, which end with a semicolon.
- The function return value is set.
- In the example, the value of the local variable
area
is returned.
Function Return Value¶
In C, a function can return only one value using the
return
statement, which is typically the result of the function or information about the success of the function's execution. The return value can be of any C variable type, except for arrays (we will come back to this). Note that the return value can be something complex like a struct
.In the example above, the function
calculate_area
returns the value of the internal variable area
, which is stored in the internal variable circle_area
in the main
function.int main() {
float circle_area = 0.0;
circle_area = calculate_area(circle_radius);
}
In C, the
main
function is a special case because it is called by the operating system or firmware, and once it finishes, the execution of the program also ends. Therefore, its return value can be used to convey information to the operating system about what happened during the program's execution, for example, whether everything went smoothly or if an error occurred.For this reason,
main
functions typically return a numeric value as their last command. By convention, zero
indicates that the program executed successfully without errors, and other return values indicate some error occurred during execution, which may not have necessarily crashed the program. Such error codes are defined by the operating system or firmware, and we will not delve deeper into them in this course.int main() {
...
// everything is ok
return 0;
}
Function Call¶
In C syntax, a function is called, as in other languages, by its name and by providing the values of the parameters.
int main() {
float circle_radius = 2.45;
circle_area = calculate_area(circle_radius);
}
In C, you need to be more precise in a function call than in some other languages.
- The function must be called with exactly the right number of arguments of the correct type. If the variables are not of the desired type, the compiler has to convert the type, and the result may not be what you intended.
- C does not allow default values for parameters.
Additionally, in a C function call, you could, for example, use another function call in place of a variable, with the return value of that function being passed as an argument to the called function. This means that functions can be chained in C.
call_me(call_a_friend(hey_return_something(you_return_this())));
int add(int a, int b) {
return a + b;
}
int multiply(int x, int y) {
return x * y;
}
int result = multiply(add(2, 3), 4); // First adds 2 and 3, then multiplies the result by 4
Embedded Functions¶
When programming embedded systems, we need to know a bit more about the inner workings of functions to be efficient programmers.
A feature of C is that the compiler creates a function's arguments as an internal variable in the function, where the value of the argument is copied. This presents a significant pitfall because embedded devices often have limited memory. This makes it (unfortunately) easy to run out of memory during program execution, typically causing the program to crash due to a memory access violation. In desktop programming, this issue is not as critical because memory is available in gigabytes versus a few kilobytes in an embedded device.
Let's examine this with an example. Below, in the
main
function, we have a variable circle_radius
, which is passed to the calculate_area
function as a parameter to the local variable radius
. Thus, the value of the argument is copied and stored in two different variables during the program's execution.int main() {
...
float circle_radius = 2.45;
circle_area = calculate_area(circle_radius);
...
}
float calculate_area(float radius) {
...
}
But let's dramatize the situation by introducing a function
send_message
, which takes a struct that contains an array of 4 bytes destination_address
and a string message of 2-kilobytes as an argument.// Define a struct for the message
typedef struct {
char destination_address[4]; // 4-byte address
char message[2048]; // 2-kilobyte message
} Message;
// Function prototype
void send_message(Message message_home);
...
// Declare a message struct and fill it with data
Message message_home = {"ABCD", "This is the message content..."};
...
// Call the function with the struct as an argument
send_message(message_home);
...
Now, in the
send_message
function, a copy of the entire 2048-character message would be made along with the 4-byte address to a local struct of the same size, consuming additional memory temporarily every time the function is called. The danger lies here: at some point during the program's execution, there may be enough memory available, but as the program progresses, memory might no longer be sufficient elsewhere. Such a silent error is difficult because the cause is not obvious to the user (or the programmer).Ouch... so how can we pass large data structures or arrays to functions? We'll return to this shortly when we introduce new variable types, and later in the material, we'll become familiar with pointer variables.
New Variable Types¶
It was noted above that local variables defined within a function exist only within the function where they are declared and only for the duration of its execution. This applies even to the
main
function, despite its otherwise special nature.We just learned that variables can be passed between functions via arguments and return values, but in addition to this, in C, we can define global and static variables.
Global Variable¶
A global variable is defined outside of all function definitions, making it accessible in all functions within the code module/file, just like the local variables in functions, except that a global variable does not need to be declared at the beginning of the function implementation.
An example of using a global variable:
/*
* Declaration of internal variables and functions in the program
*/
float oh_yes_i_am_global = 3.14;
/*
* Implementation of the main function
*/
int main() {
float i_am_local_to_main = 3.14;
printf("Print the global variable in main: %f\n", oh_yes_i_am_global );
oh_yes_i_am_global = 2.71828;
print_variables(i_am_local_to_main);
return 0;
}
/*
* Function: print_variables
*/
void print_variables(float i_am_argument) {
float i_am_local = 3.14;
printf("Print the argument in the function: %f\n", i_am_argument);
printf("Print the local variable in the function: %f\n", i_am_local );
printf("Print the global variable in the function: %f\n", oh_yes_i_am_global );
}
Note! If a global variable is redefined within a function implementation, it actually overrides the global variable declaration within the function and creates a new local variable with the same name... oops.
There are two reasons for using global variables:
- They are alive throughout the program's execution and have a broader scope in the program code, i.e., all functions in the module.
- They can be used instead of function parameters and return values to save memory. In embedded programming, it is convenient to pass information between different functions in the program without continuously creating copies of the arguments.
- This is one way to handle
structs
in embedded C programming: by making them global, you don't need to pass them as function parameters.
However, as a result, tracking down errors in the program can become more difficult because, well... they are accessible everywhere in the program, making bug tracking more challenging. Global variables also create dependencies between functionalities (functions), potentially compromising the program's modularity and leading to hard-to-understand bugs.
Finally, note that global variables can also be passed between code modules in larger C programs, but this involves significant pitfalls, so we won't cover that in detail...
Static Variable¶
A static variable is also useful in some cases. The idea is that the variable is initialized once and retains its value between function calls. In this case, the variable is declared with the
static
keyword.As an example, consider using a function's local variable as a counter.
void counter(void) {
static uint64_t count = 0;
count++;
}
In the function, the value of the
count
variable starts from the initialization value of zero and increases by one each time the function is called. Although the variable retains its value, it is still only a local variable and not accessible outside the function.For your information, the
static
keyword has other uses, but we won't cover them in this course.Conclusion¶
Regarding coding style, it's important to keep the function size (in terms of the number of program statements) reasonable. As a rule of thumb, if the function's line count doesn't fit on the screen at once, consider splitting the function into smaller functions. This will ultimately pay off in terms of debugging, maintainability, and readability of the code.
Playing with functions really kicks off when you start exploring C's standard and peripheral libraries and working on your own programs!
Give feedback on this content
Comments about this material