(Updated November 22, 2024)
Table of contents
-
Overview
Getting Started
Functions
Calling Our Functions
void
FunctionsValue-returning Functions
Parameters Revisited
Quiz
Exercises
Overview
User-defined functions are created by the application writer to support a particular feature, to fulfill a specific need that has not already been met through standard predefined functions, and whenever we may find ourselves reusing code that we were about to copy and paste elsewhere.
This chapter predicates the basic concepts of calling functions that others wrote. Consider all the functions we’ve used from the many header files presented thus far. What is significant about this is you were able to use each of these functions to do work without any additional knowledge of how the work was done, how strings or primitive types are stored, or how the printf()
function managed to get the characters to the screen.
Getting Started
Let’s reiterate a few details about functions we may or may not recall:
- They represent bits of code that perform a common task.
- They have a name representing the work to be done.
- We can call on them as often as needed.
- Sometimes, we pass additional knowledge to them.
- Sometimes, they give back results.
- We can create our own.
The last one is the key to this whole chapter. We can create our own. And it is not that difficult to do so, although we can create some complex functions. Sometimes solutions are complex.
Now let’s talk about the mechanics.
- We can call on them as often as needed.
- Sometimes, we pass additional knowledge to them.
- Sometimes, they give back results.
Functions are the C term to represent named, encapsulated work. In other programming languages, they may be known as methods or procedures, but they have the same purpose.
Functions free us from the tedium (and mistake) of copying and pasting the code everywhere we need that work to be performed.
This may sound silly, but some people are afraid to encapsulate the purpose of some code they have written and turn it into a function/function/procedure. Why? Usually, it is because the idea is new (resistance to change is normal), they are not confident with building them, or they struggle with the mechanics.
So, we will now put together the details of how. Consider this:
int x;
x = 2 + 5;
We are basically saying x
gets the value of the sum of 2
and 5
.
What if we wrote it this way?
int x;
x = sum(2, 5);
We have not changed the meaning of what we are doing. All that has changed is the how. Instead of using an operator, we are using a function.
But, this function does not yet exist. So we will create it.
int sum(int a, int b) {
int t;
t = a + b;
return t;
}
Now let us describe in detail what is happening here.
- Line 1 – Declares a function called
sum
that returns anint
and accepts twoint
s (a
andb
) as parameters. This means our function is expecting twoint
s to be passed in the parenthesis and it will will give back oneint
. - Line 3 – Declares an
int
variablet
. This will be used to calculate the total and it a temporary throw-away variable. - Line 5 –
t
gets the value of the sum of the two parameters,a
andb
. - Line 7 – The function
return
s the result (t
) back to the caller. This is the mechanism that give back the result.
To understand how this all comes together, we have to consider the idea of abstraction. We do not know what will be sent as values (arguments) for a
and b
. But we do not need to know what will happen in the future. All we need to know is how to access those values when the time comes when the function is invoked.
For this example we have decided that the first value passed will be a
and the second is b
and the result stored in t
. We could have called them potato
and guava
and assigned them to pickle
and returned the value stored in pickle
.
See?
int sum(int potato, int guava) {
int pickle;
pickle = potato + guava;
return pickle;
}
Sure, it is hilarious, and it proves an important point. The function, variable, and parameter names can be anything (except the set of reserved words, of course!), but do your best to pick meaningful names when you can and not ones that detract from the meaning of your code.
Functions
Since the start of this book, we have covered a bit of ground on objects, functions, wrappers, and the possible immutability of an object; it is finally time to create our functions.
In the previous section, we delved into the visual aspect of function creation. However, there is still some unfinished business. Why? Well, we defined a function called sum()
and showed how it could be invoked, but we did not finish painting the complete picture of how the whole program looked when it was finished. This is presented now.
#include <stdio.h>
/* function declaraion */
int sum(int a, int b) {
int t;
t = a + b;
return t;
}
/* Main function */
int main(int argc, char** argv) {
int x;
/* sum invoked here. */
x = sum(2,5);
printf("The sum of 2 and 5 is %d.\n", x);
}
Example GS1: Fully realized Getting Started example of user-defined functions.
And the slightly more humorous one:
#include <stdio.h>
/* function declaraion */
int sum(int potato, int guava) {
int pickle;
pickle = potato + guava;
return pickle;
}
/* Main function */
int main(int argc, char** argv) {
int x;
/* sum invoked here. */
x = sum(2,5);
printf("The sum of 2 and 5 is %d.\n", x);
}
Example GS2: Fully realized Getting Started example of user-defined functions.
We will build a slightly more involved version of creating functions for specific purposes. The goal here is to move beyond the introductory phase and begin writing many functions of various forms.
Let us look at Example 1, which will demonstrate the use of all the different forms of function design.
#include <stdio.h>
#include <time.h>
#include <stdlib.h>
// forward declarations
void announce(void);
void print_error(const char *);
int get_random(void);
int power(int, int);
int main(int argc, char *argv[]) {
announce();
printf("%d\n", get_random());
printf("%d\n", power(2,10));
print_error("something terrible");
}
// no args, no return
void announce(void) {
srand(time(NULL));
fprintf(stdout, "The process has begun!\n");
}
// some args no return
void print_error(const char *errstr) {
fprintf(stderr, "The program encountered a \"%s\" error.\n", errstr);
}
// no args, has return
int get_random(void) {
return rand();
}
// args and return
int power(int base, int pwr) {
int x, total=1;
if ( pwr == 0 )
return 1;
else {
for ( x = 1; x <= pwr; x++ )
total *= base;
}
return total;
}
Example 1: C program demonstrating functions.
In Example 1, we introduce the four types of functions you can create. It is easier to reveal all of the details at once. There is nothing to hide here, and you have already seen examples of each type. Now we will take the time to identify each kind.
There are two fundamental types of functions:
- Value-returning functions.
- Void functions. (Void functions return no value)
Then it is simply an academic exercise that some functions require arguments while others do not. This makes four possible functions.
The main()
function is a value-returning function – we have seen this for quite some time with the int
keyword to the left of main(...)
. The functions announce()
and print_error()
are void
functions, however print_error()
takes arguments while announce()
does not.
Here is sample output generated by Example 1:
The process has begun! 83 1024 The program encountered a "something terrible" error.
Now we need to clear up some terminology. When discussing any functions (user-defined or from a library), two different terms describe what we put in the parenthesis. When we write the function and the work it will do, we define required parameters. Remember that not all functions have parameters. When we invoke the function, the values we send are arguments.
- Parameters define what is required by the function.
- Arguments define what we actually sent to the function.
Calling Our Functions
When using one of our functions or predefined functions in a program by name, it is known as a function call. To call a function is to invoke it or make it perform its work. You will likely see many pieces of documentation use the terms call and invoke interchangeably.
Note that when we declare any of the functions shown, they are not called as a result of their mere presence. This is also true of the main()
function; that is, the main()
function is not called until the runtime environment invokes it. Remember that the main()
function is the starting point when we run the program.
We explicitly call functions by using their names in or as a statement. This happens in main()
with the block shown below.
announce();
printf("%d\n", get_random());
printf("%d\n", power(2,10));
print_error("something terrible");
Do you see the six functions being invoked? (Lines 2 and 3 have nested function calls. The inner call's return value feeds the required argument(s) for the outer call. So, there are two function invocations on each line.)
Without these function call statements in our code, little would happen concerning computation, input, or output. Remember this detail when writing your functions – if you do not call it, it does not occur.
static
modifier on a function can change its scope. Using the static
modifier allows the function to be known only in the translation unit it is defined. In other words, it is not global (the default) and will not be known outside the .c
file in which it’s defined.
static void print_error(const char *errstr) {
fprintf(stderr, "The program encountered a \"%s\" error.", errstr);
}
This can be useful if you have a function for housekeeping purposes or maybe internal to a library you do not want as part of the published API and should only be known to the global functions in the same translation unit.
Void functions
The void
functions are the easiest to get a handle on since we have been creating one void
function in particular for some time, that is, the main()
function. We have also been using several void functions in our programs for output (memset
, free
, printf
). The void
functions do not return anything to the caller (which is the opposite of malloc
or isalpha
, where we expect some result). They simply do their work, and then control is passed back to wherever they had been called.
Our announce()
and print_error()
are also void
functions. Let us look at print_error()
in a bit more detail. We have a void
function that requires parameters.
void print_error(const char *errstr)
This indicates that print_error()
is a function that requires precisely one parameter (labeled as errstr
). The parameter is required to be of type const char *
, which means it is a string (char *
) that cannot be modified (const
).
Each time we call our function, we do something like this:
print_error("Something happened.")
We provide the argument to be used. So the value "Something happened."
is the argument, and errstr
is the parameter.
The print_error()
function does its job of printing the value of the argument, then ends, and control is returned to the caller.
Value-returning functions
After discussing void
functions, value-returning functions should be a snap. The first step in creating value-returning functions is to turn the void
into some other type. This type can be any primitive type or a pointer. After that, it is a matter of adding the return
statement to send the results back to the caller. As seen in many other examples, one of the greatest benefits of value-returning functions is using them in expressions or as arguments to functions.
One thing that should be mentioned is that functions do not need to print something. Sure, many of our examples do, but many standard functions don't print anything. This is quite common as functions are frequently used to help with computation or move data around. And, yes, some are I/O based, which includes our prior use of printf()
.
You may recall that functions like isalpha()
, strchr()
, and strdup()
do not print anything, they simply return the requested information. Of course, that value is then stored in some other variable. We will now look at the mechanism that is involved in getting these results sent back. The value-returning function, get_random()
, from Example 1 is reiterated here.
int get_random(void) {
return rand();
}
The get_random()
function demonstrates the use of calling another value-returning function to assist in the work to be done. This, too, is quite common in user-defined functions. While we may be creating a new tool that does not exist, it does not preclude the idea of making our code simpler by using other available tools. This function is designed to return a random value in the range 1-100.
get_random()
is clear in its intention to create a random number in the range 1-100, it is not at all obvious by its name.The return
statement is the mechanism that sends the value back to the caller. All functions eventually return, but only value-returning functions contain a statement expressing precisely what is to be sent back.
void
functions do not have a return statement to send back results, the return
statement can still be used a means of leaving the function immediately as long as there is no attempt to send back a value.Another concept that get_random()
demonstrates is the idea of performing the calculations, including additional function calls, on the same line as the return
statement. Remember that expressions do not have to be after an assignment operator.
To clarify, the get_random()
function could have been written like the following:
int get_random() {
int r;
r = rand() % 100 + 1);
return r;
}
Note the declaration of local variable, r
, the assignment of the random number into r
and then the return
statement indicating that the value in r
is to be returned. It is very reminiscent of the code in Getting Started. However, on a certain level, very unnecessary. Much like in mathematics and even in real life, we identify steps and/or representations that are simply not necessary, so we do not perform them. We refine or reduce the process to its simplest form. That has been done to get_random()
.
Now, on to power()
!
int power(int base, int pwr) {
int x, total=1;
if ( pwr == 0 )
return 1;
else {
for ( x = 1; x <= pwr; x++ )
total *= base;
}
return total;
}
This is the one function with the most code. It is attempting to calculate a value raised to an exponent by performing a loop. The takeaways in this code are not new, and we will reiterate them here:
- The parameters (
base
andpwr
) are accessible to the code as regular variables. They hold the contents of what was passed. - There are local variables (
x
andtotal
) just like we do inmain()
. - There are two
return
statements. This is actually quite common. - One
return
is conditional while the other is unconditional.
power()
function is also incomplete in its implementation. It does not handle negative exponents, nor does it handle any errors resulting in their use. The example is intentionally lacking in some features for simplicity.Do not let the two return
statements trip you up. You can have as many as needed to keep the code simple. We could have assigned total
the value of 1 and fallen through the if
test to the final return
. Generally, when writing functions we want to leave as soon as we realize the work is done. Hence the second return
inside the if
.
Regardless of your code arrangement, a return
statement MUST always be reachable. The final return
could also have been moved into the else
clause. As long as there is a reachable return
statement for every conditional possibility, the compiler will accept the function as complete.
Parameters Revisited
As we discussed previously, we can have parameters of any primitive type. This is known as call-by-value or pass-by-value. Meaning a copy is passed to the called function.
We can also have pointers as parameters. Remember, pointers are addresses where the data is, not the data itself. This is still call-by-value - this is not the same as call-by-reference as in C++.
Consider these rules when declaring and using parameters:
- When passing primitive type variables as arguments, the parameter contains a copy of the data in the argument. Nothing will happen to the data in the argument.
- When passing pointers as arguments, the parameter contains a copy of the address in the argument. This means that changes can be made using that address to alter data at the original location.
When dealing with char*
as arguments, we must be careful how we use them. In Example 2, we look at a string passed as an argument and then reverse its contents.
#include <stdio.h>
#include <errno.h>
#include <string.h>
int reverse(char *s) {
if (!s) return 0;
int l = strlen(s);
if ( l < 1 ) return 0;
int m = l / 2;
for (int x = 0; x < m; x++) {
char t;
t = s[x];
s[x] = s[l-x-1];
s[l-x-1] = t;
}
return 1;
}
int main(int argc, char *argv[]) {
char line[] = "Time to save the day!";
printf("%s\n", line);
if ( reverse(line) )
printf("%s\n", line);
else
printf("Well, something was not good...\n");
}
Example 2: Program demonstrating passing addresses by value.
Here we pass a string from main()
as an argument to the reverse()
function. We want to demonstrate that passing the address allows us to change the original string. We also want to mention some dangers that may arise.
Remember that some of the string functions have been banned as unsafe. This is due to the lack of safe bounds checking in those functions. They can be made safe, but the original semantics of the functions prohibits such a change. Thus we mark them as unsafe. The reverse()
function is pretty safe. Its purpose is well-defined, and we are expected to modify the original string in a predetermined way.
The output of Example 2 is as follows:
Time to save the day! !yad eht evas ot emiT
char*
to be const char*
. This signals the compiler that it is read-only and that no changes will be made.Now we will look at another function that uses addresses passed by value. This is the obligatory swap function that is intended to swap the places of two values.
#include <stdio.h>
#include <errno.h>
#include <string.h>
void swap(char *a, char *b) {
char t = *a;
*a = *b;
*b = t;
}
int reverse(char *s) {
if (!s) return 0;
int l = strlen(s);
if ( l < 1 ) return 0;
int m = l / 2;
for (int x = 0; x < m; x++)
swap(&s[x], &s[l-x-1]);
return 1;
}
int main(int argc, char *argv[]) {
char line[] = "Time to save the day!";
printf("%s\n", line);
if ( reverse(line) )
printf("%s\n", line);
else
printf("Well, something was not good...\n");
}
Example 3: Using a swap function to exchange two values.
The notation of the swap()
function is simple. The asterisk, or dereferencing operator, is used to access the data at the address. Remember that a
and b
represent addresses, so *a
and *b
represent the values at those addresses, respectively.
We will talk more about pointers in Chapter 7.
Quiz
Exercises
- (Easy) Create a function called
sum
that sums twoint
values. - (Easy) Create a function called
concat
that takes twoString
values and returns the concatenation of the first followed by the second. - (Intermediate) Create a two more
sum()
functions (overloaded) that each take twoshort
and two long values. Adjust the return type as appropriate. - (Intermediate) Recreate the standard
String
function,indexOf()
, by creating a function that takes aString
and achar
to be sought within theString
. Return the position of the first occurrence or-1
if not found. - (Intermediate) Modify #4 to create
rindexOf()
, still taking aString
and achar
to be sought within theString
from the right end of the string. Return the first position or-1
if not found. - (Advanced) Write a function that takes a
String
, a sourcechar
and replacementchar
. Return theString
with the characters replaced. - (Expert) Write a pair of overloaded functions that convert a String to an int. You will simulate the
Integer
functionsparseInt(String)
andparseInt(String, int)
. The former assumes a base 10 number while the latter provides a radix. Return the converted value as anint
. - (Expert) Write a function (or functions) that will take dimensions as parameters
width
andheight
. With these values, create a dimension grid using the hyphen, pipe, and space characters. A 5 x 8 grid would look something like this:--------------------- | | | | | | --------------------- | | | | | | --------------------- | | | | | | --------------------- | | | | | | --------------------- | | | | | | --------------------- | | | | | | --------------------- | | | | | | --------------------- | | | | | | ---------------------