Skip to content

6.1. Function Basics

Fundamental

A function definition typically consists of a return type, a name, a list of zero or more parameters, and a body. The parameters are specified in a comma-separated list enclosed in parentheses. The actions that the function performs are specified in a statement block (§ 5.1, p. 173), referred to as the function body.

We execute a function through the call operator, which is a pair of parentheses. The call operator takes an expression that is a function or points to a function. Inside the parentheses is a comma-separated list of arguments. The arguments are used to initialize the function’s parameters. The type of a call expression is the return type of the function.

Writing a Function

As an example, we’ll write a function to determine the factorial of a given number. The factorial of a number n is the product of the numbers from 1 through n. The factorial of 5, for example, is 120.

c++
1 * 2 * 3 * 4 * 5 = 120

We might define this function as follows:

c++
// factorial of val is val * (val - 1) * (val - 2) . . . * ((val - (val - 1)) * 1)
int fact(int val)
{
    int ret = 1; // local variable to hold the result as we calculate it
    while (val > 1)
        ret *= val--;  // assign ret * val to ret and decrement val
    return ret;        // return the result
}

Our function is named fact. It takes one int parameter and returns an int value. Inside the while loop, we compute the factorial using the postfix decrement operator (§ 4.5, p. 147) to reduce the value of val by 1 on each iteration. The return statement ends execution of fact and returns the value of ret.

Calling a Function

To call fact, we must supply an int value. The result of the call is also an int:

c++
int main()
{
    int j = fact(5);  // j equals 120, i.e., the result of fact(5)
    cout << "5! is " << j << endl;
    return 0;
}

A function call does two things: It initializes the function’s parameters from the corresponding arguments, and it transfers control to that function. Execution of the calling function is suspended and execution of the called function begins.

Execution of a function begins with the (implicit) definition and initialization of its parameters. Thus, when we call fact, the first thing that happens is that an int variable named val is created. This variable is initialized by the argument in the call to fact, which in this case is 5.

Execution of a function ends when a return statement is encountered. Like a function call, the return statement does two things: It returns the value (if any) in the return, and it transfers control out of the called function back to the calling function. The value returned by the function is used to initialize the result of the call expression. Execution continues with whatever remains of the expression in which the call appeared. Thus, our call to fact is equivalent to the following:

c++
int val = 5;       // initialize val from the literal 5
int ret = 1;       // code from the body of fact
while (val > 1)
    ret *= val--;
int j = ret;       // initialize j as a copy of ret

Parameters and Arguments

Arguments are the initializers for a function’s parameters. The first argument initializes the first parameter, the second argument initializes the second parameter, and so on. Although we know which argument initializes which parameter, we have no guarantees about the order in which arguments are evaluated (§ 4.1.3, p. 137). The compiler is free to evaluate the arguments in whatever order it prefers.

The type of each argument must match the corresponding parameter in the same way that the type of any initializer must match the type of the object it initializes. We must pass exactly the same number of arguments as the function has parameters. Because every call is guaranteed to pass as many arguments as the function has parameters, parameters are always initialized.

Because fact has a single parameter of type int, every time we call it we must supply a single argument that can be converted (§ 4.11, p. 159) to int:

c++
fact("hello");       // error: wrong argument type
fact();              // error: too few arguments
fact(42, 10, 0);     // error: too many arguments
fact(3.14);          // ok: argument is converted to int

The first call fails because there is no conversion from const char* to int. The second and third calls pass the wrong number of arguments. The fact function must be called with one argument; it is an error to call it with any other number. The last call is legal because there is a conversion from double to int. In this call, the argument is implicitly converted to int (through truncation). After the conversion, this call is equivalent to

c++
fact(3);

Function Parameter List

A function’s parameter list can be empty but cannot be omitted. Typically we define a function with no parameters by writing an empty parameter list. For compatibility with C, we also can use the keyword void to indicate that there are no parameters:

c++
void f1(){ /* ... */ }     // implicit void parameter list
void f2(void){ /* ... */ } // explicit void parameter list

A parameter list typically consists of a comma-separated list of parameters, each of which looks like a declaration with a single declarator. Even when the types of two parameters are the same, the type must be repeated:

c++
int f3(int v1, v2) { /* ... */ }     // error
int f4(int v1, int v2) { /* ... */ } // ok

No two parameters can have the same name. Moreover, local variables at the outermost scope of the function may not use the same name as any parameter.

Parameter names are optional. However, there is no way to use an unnamed parameter. Therefore, parameters ordinarily have names. Occasionally a function has a parameter that is not used. Such parameters are often left unnamed, to indicate that they aren’t used. Leaving a parameter unnamed doesn’t change the number of arguments that a call must supply. A call must supply an argument for every parameter, even if that parameter isn’t used.

Function Return Type

Most types can be used as the return type of a function. In particular, the return type can be void, which means that the function does not return a value. However, the return type may not be an array type (§ 3.5, p. 113) or a function type. However, a function may return a pointer to an array or a function. We’ll see how to define functions that return pointers (or references) to arrays in § 6.3.3 (p. 228) and how to return pointers to functions in § 6.7 (p. 247).

6.1.1. Local Objects

Fundamental

In C++, names have scope (§ 2.2.4, p. 48), and objects have lifetimes. It is important to understand both of these concepts.

  • The scope of a name is the part of the program’s text in which that name is visible.
  • The lifetime of an object is the time during the program’s execution that the object exists.

As we’ve seen, the body of a function is a statement block. As usual, the block forms a new scope in which we can define variables. Parameters and variables defined inside a function body are referred to as local variables. They are “local” to that function and hide declarations of the same name made in an outer scope.

INFO

Exercises Section 6.1

Exercise 6.1: What is the difference between a parameter and an argument?

Exercise 6.2: Indicate which of the following functions are in error and why. Suggest how you might correct the problems.

(a)int f() {

(b)f2(int i) { /* ... */ }

(c)int calc(int v1, int v1) /* ... */ }

(d)double square(double x) return x * x;

Exercise 6.3: Write and test your own version of fact.

Exercise 6.4: Write a function that interacts with the user, asking for a number and generating the factorial of that number. Call this function from main.

Exercise 6.5: Write a function to return the absolute value of its argument.

Objects defined outside any function exist throughout the program’s execution. Such objects are created when the program starts and are not destroyed until the program ends. The lifetime of a local variable depends on how it is defined.

Automatic Objects

The objects that correspond to ordinary local variables are created when the function’s control path passes through the variable’s definition. They are destroyed when control passes through the end of the block in which the variable is defined. Objects that exist only while a block is executing are known as automatic objects. After execution exits a block, the values of the automatic objects created in that block are undefined.

Parameters are automatic objects. Storage for the parameters is allocated when the function begins. Parameters are defined in the scope of the function body. Hence they are destroyed when the function terminates.

Automatic objects corresponding to the function’s parameters are initialized by the arguments passed to the function. Automatic objects corresponding to local variables are initialized if their definition contains an initializer. Otherwise, they are default initialized (§ 2.2.1, p. 43), which means that uninitialized local variables of built-in type have undefined values.

Local static Objects

It can be useful to have a local variable whose lifetime continues across calls to the function. We obtain such objects by defining a local variable as static. Each local static object is initialized before the first time execution passes through the object’s definition. Local statics are not destroyed when a function ends; they are destroyed when the program terminates.

As a trivial example, here is a function that counts how many times it is called:

c++
size_t count_calls()
{
    static size_t ctr = 0;  // value will persist across calls
    return ++ctr;
}
int main()
{
    for (size_t i = 0; i != 10; ++i)
        cout << count_calls() << endl;
    return 0;
}

This program will print the numbers from 1 through 10 inclusive.

Before control flows through the definition of ctr for the first time, ctr is created and given an initial value of 0. Each call increments ctr and returns its new value. Whenever count_calls is executed, the variable ctr already exists and has whatever value was in that variable the last time the function exited. Thus, on the second invocation, the value of ctr is 1, on the third it is 2, and so on.

If a local static has no explicit initializer, it is value initialized (§ 3.3.1, p. 98), meaning that local statics of built-in type are initialized to zero.

INFO

Exercises Section 6.1.1

Exercise 6.6: Explain the differences between a parameter, a local variable, and a local static variable. Give an example of a function in which each might be useful.

Exercise 6.7: Write a function that returns 0 when it is first called and then generates numbers in sequence each time it is called again.

6.1.2. Function Declarations

Fundamental

Like any other name, the name of a function must be declared before we can use it. As with variables (§ 2.2.2, p. 45), a function may be defined only once but may be declared multiple times. With one exception that we’ll cover in § 15.3 (p. 603), we can declare a function that is not defined so long as we never use that function.

A function declaration is just like a function definition except that a declaration has no function body. In a declaration, a semicolon replaces the function body.

Because a function declaration has no body, there is no need for parameter names. Hence, parameter names are often omitted in a declaration. Although parameter names are not required, they can be used to help users of the function understand what the function does:

c++
// parameter names chosen to indicate that the iterators denote a range of values to print
void print(vector<int>::const_iterator beg,
           vector<int>::const_iterator end);

These three elements—the return type, function name, and parameter types—describe the function’s interface. They specify all the information we need to call the function. Function declarations are also known as the function prototype.

Function Declarations Go in Header Files

Recall that variables are declared in header files (§ 2.6.3, p. 76) and defined in source files. For the same reasons, functions should be declared in header files and defined in source files.

It may be tempting—and would be legal—to put a function declaration directly in each source file that uses the function. However, doing so is tedious and error-prone. When we use header files for our function declarations, we can ensure that all the declarations for a given function agree. Moreover, if the interface to the function changes, only one declaration has to be changed.

The source file that defines a function should include the header that contains that function’s declaration. That way the compiler will verify that the definition and declaration are consistent.

TIP

Best Practices

The header that declares a function should be included in the source file that defines that function.

INFO

Exercises Section 6.1.2

Exercise 6.8: Write a header file named Chapter6.h that contains declarations for the functions you wrote for the exercises in § 6.1 (p. 205).

6.1.3. Separate Compilation

Fundamental

As our programs get more complicated, we’ll want to store the various parts of the program in separate files. For example, we might store the functions we wrote for the exercises in § 6.1 (p. 205) in one file and store code that uses these functions in other source files. To allow programs to be written in logical parts, C++ supports what is commonly known as separate compilation. Separate compilation lets us split our programs into several files, each of which can be compiled independently.

Compiling and Linking Multiple Source Files

As an example, assume that the definition of our fact function is in a file named fact.cc and its declaration is in a header file named Chapter6.h. Our fact.cc file, like any file that uses these functions, will include the Chapter6.h header. We’ll store a main function that calls fact in a second file named factMain.cc. To produce an executable file, we must tell the compiler where to find all of the code we use. We might compile these files as follows:

shellscript
$ CC factMain.cc fact.cc   # generates factMain.exe or a.out
$ CC factMain.cc fact.cc -o main # generates main or main.exe

Here CC is the name of our compiler, $ is our system prompt, and # begins a command-line comment. We can now run the executable file, which will run our main function.

If we have changed only one of our source files, we’d like to recompile only the file that actually changed. Most compilers provide a way to separately compile each file. This process usually yields a file with the .obj (Windows) or .o (UNIX) file extension, indicating that the file contains object code.

The compiler lets us link object files together to form an executable. On the system we use, we would separately compile our program as follows:

shellscript
$ CC -c factMain.cc     # generates factMain.o
$ CC -c fact.cc         # generates fact.o
$ CC factMain.o fact.o  # generates factMain.exe or a.out
$ CC factMain.o fact.o -o main # generates main or main.exe

You’ll need to check with your compiler’s user’s guide to understand how to compile and execute programs made up of multiple source files.

INFO

Exercises Section 6.1.3

Exercise 6.9: Write your own versions of the fact.cc and factMain.cc files. These files should include your Chapter6.h from the exercises in the previous section. Use these files to understand how your compiler supports separate compilation.