A "Feild" Guide to Programming with C++
Introductory programming notes by Henry FeildChapter 7: Functions
Important takeaways
- functions take input and return outputs and/or have side effects like printing to the terminal or modifying memory
- every function has a signature in the form: return-type name(parameters)
- the three things every function should have:
- declaration (above main)
- definition (below main)
- invocation (inside of any function)
- data can be passed to a function in two ways:
- pass by value—values are copied from the calling function to the invoked function
- pass by reference—memory is linked between the calling function and invoked function
- arrays are passed a little differently; we treat 1D arrays differently than multi-dimensional arrays
Contents
An abstract introduction to functions
We use the term function in programming similar to the way Mathematicians use it. For example, the mathematical function sqrt(x) takes one parameter, x, which is a real number, and then returns the square root of that number. In programming, a function is a way of encapsulating some set of operations over a set of inputs and produces an output and/or causes a side effect (displays something, writes something to disk, or modifies memory). In a program, we might make a function to prompt a user with a specified message and return the value read in. Or we might make a function that validates an input. Generically we refer to the inputs as parameters and the output is the return value.
So why do we use functions? There are a lot of reasons, actually. Let's start by considering this example. If I wrote code to take the square root of a number, and didn't use a function, then every time I wanted to take a square root, I'd have to write all of those steps. This violates a couple tenants of programming, the first of which is Don't Repeat Yourself (DRY)—redundancy (e.g., the exact same code repeated over and over again) is bad. Second, cognitively it's much harder to look at a set of steps and comprehend what's going on. So using functions allows your code to be more concise, readable, and understandable.
(Back to top)Elements of a C++ function
In C++, functions have three parts: declarations,
definitions, and invocations. Functions should be declared above
main
and defined below. The reason for the former is that functions
must be declared before they can be invoked; the compiler, which reads through
your code line by line, doesn't know what to do if you use a function it hasn't
seen a declaration for yet. We put the definitions below in this class just as a
convention—in future classes or your place of work, you may find that this
is not the case. Invocations can occur in any function, including
main
.
Signature
Functions are described by their signature, which
consists of the return type, name, and parameters. Here's an
the signature for main
:
Here's the signature of a function named printName
that doesn't
return anything, but takes a string as a parameter:
Here's the signature of a function named findMax
that returns the
maximum of three float parameters:
Note that if no value is returned, a return type of void is specified. For each parameter, the signature must include the type and a name that will be used to refer to that parameter within the function. The order matters—whatever order you put the parameters in in your signature, you must use the same order in your declaration, definition, and invocation. Functions are allowed to take no parameters. If a function takes more than one parameter, they should be separated by commas.
Declaration
Declarations consist of the signature followed by a ;
. Technically,
the parameter names do not have to be present—the purpose of the
declaration is to let the compiler know that the a function with this name,
return type, and parameter types will be defined and used at some point later in
the program. So, both of the following are valid declarations.
In this class, we will follow the first style and include the parameter names.
Definition
Definitions consist of the signature followed by a pair of curly braces. If the
signature includes a return type other than void, then the function should
always return something of that type using the return
statement.
Here are a couple examples.
Unlike function declarations, you must include the parameter names in a function definition. The comment above each function is what we call a JavaDoc comment. It tells us what the function does, what the parameters are, and what's returned, if anything. Be sure to take a look at the style guide for information about the format of these comments as you will be expected to include them above all of your functions.
Invocation
Invocations consist of the function name, a pair of parentheses containing the
variables or values you want to correspond to each parameter from the signature,
in the same order, separated by commas. When invoking, the values we pass in
are called arguments. The names of
arguments do not need to be the same as the parameter
names!!! Though they can be. Function's that return a value can be
treated like any value—the returned value will essentially replace
wherever the function is invoked. So, for instance, we can assign the result of
a function to a variable, or print out the result of a function directly using
cout
. Here's and example of invoking our functions from
main
:
It's crucial to understand what happens when you invoke a function in terms of
memory. The function in which an invocation is made, e.g., main
in
the example above, is called the caller. New memory is
allocated for the function to do its thing. This new memory includes space for
all of the parameters and variables declared within the function. The arguments
passed from the caller to the function are copied over to this memory, and are
stored under the parameter names. The program control is then handed over to the
functions and the function code executes line by line. Once the function has
finished or returns a value, program control is given back to the caller, along
with the return value if there is one. Once the function has returned control,
it and all of its variables (including the parameters) are removed from memory.
One consequence of this is that function instances are completely independent of
each other—if I invoke printName
multiple times, each
invocation has no knowledge or shared variables with any of the other
invocations.
Putting it all together
Here's a full example of declaring, defining, and invoking a function in a C++ program:
To better understand the changes to program control and memory during the
execution of a function, here is a play-by-play description using a slightly
shorter example. Here, we are storing the value "Hank"
in a
variable named myName
in main
before invoking
printName
with that variable as an argument.
Start
The initial program.
Step 1
The main function is invoked by the system; space is made for all of
main
's variables and we keep track of the return value (an
int
).
Step 2
The string "Hank"
is stored in the variable myName
inside of main
's memory.
Step 3
The printName
function is invoked. When this happens, new memory is
created for the function and an entry is made for every parameter and variable
declared within the function. The function does not return anything, so we do
not need to worry about keeping track of a return value. The values of any
variables or literal values passed in as arguments to a function during
invocation are then copied to the memory reserved for those parameters in the
function's memory space. In this case, the variable myName
in
main
was passed as an argument. Therefore, we take the value of
that variable, "Hank"
, and copy it to the space for
the corresponding parameter in the function signature, which is
name
.
Step 4
The program control now enters the actual function (line 14).
That line causes the following to be output to the terminal:
Name: Hank
Step 5
Line 14 is the final line of the function, so after its execution, program
control goes back to the calling function, i.e., main
. Note that
all of the function's memory is removed.
Step 6
The next line in main
is executed, which is a return statement. We
will store this value in the "return value" section of main
's
memory block and this will tell us what value to give to main
's
caller (i.e., the system).
Finished
The program has finished execution.
(Back to top)Pass-by-value
In the functions above, we're passing parameters by value. That means that the values of variables passed in as arguments are copied into the memory designated explicitly for the corresponding function parameters. That also means that if you assign a new value to a parameter within a function, that reassignment will not be reflected outside of the function. Here's an example:
This will print:
Hank Hank(Back to top)
Pass-by-reference
Sometimes it's helpful to have a function reassign the value of an argument
passed in. In order to do this, you need arguments to be passed by reference. This involves adding an ampersand
(&
) prior to parameter names in the function signature (so your
declaration and definition will be affected). You don't need to do anything
special in the function or in the invocation. Here's an example where we use a
pass by reference parameter, name
, to change the value of the
argument passed in to "Bob"
:
This will print:
Hank Bob
Here is the memory diagram for a slightly modified version of the program above.
This only shows the step when the resetName
function is invoked and
memory is created for the function instance. We can see that the function's
name
parameter is directly linked to the value of the variable that
was passed in (i.g., myName
).
In order to use pass by reference, the corresponding argument must be a variable—not a literal or other value. The reason is has to do with the fact that the function must be pointed to a segment of memory, which is only possible when the argument is a variable. Literals and other values computed from expressions are ephemeral and do not reside in memory, so they cannot be linked to.
Now, lets suppose that you don't want to allow changes to the data
that's passed in. That may seem funny, since we motivated using pass-by-
reference so that we could change the underlying value. But there are
reasons a programmer may want to use pass-by-reference without allowing changes,
such as not wasting memory by uselessly copying structures over and over again.
So, if you want to not copy the data, but you also don't want to be able to
change the value, then there is a special keyword that precedes the parameter
type in the signature: const
. If you try to alter the value of a
parameter that is const
, then the compiler will get mad at you.
Here's an example:
Passing arrays to functions
Passing static 1D and multi-dimensional arrays to functions is different from passing regular variables, so we will look at how to do that in this section. The 1D arrays are handled differently from multi-dimensional arrays, and so I'll dedicate a separate section to each.
Static 1D arrays
There are two important things to remember when creating a function that will take an array as a parameter. First, to specify a 1D array as a parameter in a function signature, use the format: type *arrayName. We'll lean more about what the asterisk means when we get to pointers. Second, always include a parameter to store the size of the array. This is crucial since without it, the function will have no way of knowing what the size is. Here's an example.
Static multi-dimensional arrays
This example expands the sample program from
when we learned about static multi-dimensional arrays by creating a function
that takes a 2D array with specific sizes and displays it. Two things of note.
First, I've moved the constants outside of main (to make them global) and above
the function declarations; this is because we will use the constants in main as
well as the function declaration and definition for
print2DStaticArray
. Second, we must specify each dimension in the
function signature, and those dimensions are used throughout the function.