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.

With parameter names.
Without parameter names.

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:

(Back to top)

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.

(Back to top)