Functions
A function is a named, independent section of code that performs a specific task and optionally returns a computed value. Variable scope refers to the part of a program where a particular variable is declared and can be used.
Table of Contents
- Functions
- Function Definition and Execution
- Function Parameters and Arguments
- Return Values
- Whitespace
- Forward Declaration
- Functions and Header Files
- What to Return from Main
- Variable Scope
- Static Local Variables
- Pass by Value
- Pass By Const Reference
- Pass by Reference
- In-Out Parameters
- Multiple Out Parameters
- Function Overloading
- Returning Different Types
Functions
Functions are the verbs of computer programming. They do things.
They increase:
- Modularity, by allowing us to break down large programs into smaller parts.
- Reusability, by allowing us to reuse code without having to retype or copy/paste it.
- Readability, by allowing us to give descriptive names to specific parts of our program.
Function Definition and Execution
Functions are defined in C++ with a return type, a name, and an optional list of input parameters.
type functionName(type parameter1, type parameter2) {
// function body
}
A function with a return type of void
does not return a value.
void sayGoodnight() {
std::cout << "So Long\nFarewell\n";
}
// To execute this function:
sayGoodnight();
Function Parameters and Arguments
Functions can be defined to take one or more arguments by way of a parameter list.
Default values for the parameters can also be specified.
void sayGoodnightRepeatedly(std::string name, int numberOfTimes = 1) {
for (int i{0}; i < numberOfTimes; i++) {
std::cout << "Goodnight " << name << "\n";
}
}
sayGoodnightRepeatedly("Wally", 12);
sayGoodnightRepeatedly("Wally"); // Second argument defaults to 1.
Return Values
Functions can optionally return a value to the caller.
std::string pizzaMessage(int piecesLeft, int hungryPeople) {
if (piecesLeft < hungryPeople) {
return "Sorry we don't have enough pizza.";
} else {
return "Let's share! Any leftovers go to the dog.";
}
}
std::cout << pizzaMessage(13, 7) << "\n";
Whitespace
Because C++ ignores whitespace you’ll sometimes see function calls spread over multiple lines.
This is done for readability purposes and helps avoid extra-long lines of code.
functionName(argumentOne,
argumentTwo,
argumentThree,
argumentFour);
Forward Declaration
Functions need to be declared before they can be used.
Try to compile/run this program:
Instead of moving the fetchInteger
function above the main
function, we can forward declare fetchInteger
. Uncomment the forward declaration to fix the above code.ss
🎵 Note:
Parameter names are optional when forward declaring a function.
In the above program the forward declaration could have been:
int fetchInteger(std::string);
Functions and Header Files
It’s common to forward declare functions defined in separate .cpp
source files using .h
header files.
First the function is forward declared in a header with include guards:
#ifndef USERINPUT_H
#define USERINPUT_H
int fetchInteger(std::string prompt);
#endif
The function can now be implemented in a .cpp
file separate from where main()
is defined. Both .cpp
files must then #include
the associated .h
header file.
Navigate through the example files using the left pane “Files” explore:
What to Return from Main
The main()
function is the only function with a non-void
type where we don’t have to explicitly return
:
int main() {
int goatCount = 12;
}
The main()
function is your program’s entry point. The value returned by main()
is called a status code or exit code, and it’s sent to the Operating System when the program exits.
Non-zero status codes are used to indicate program failure.
If no return
is present it’s equivalent to returning 0
, meaning success.
The <cstdlib>
header defines two status codes you can use when returning from main()
:
EXIT_SUCCESS
EXIT_FAILURE
Variable Scope
Like other {}
blocks, C++ functions each have their own scope.
std::string gemFactory(int numberOfGems) {
// The gems variable is a local variable.
std::string gems = std::string(numberOfGems, '💎'); // gems variable is scoped to function.
return gems;
}
int main() {
std::cout << gemFactory(5) << "\n";
std::cout << gems; // Error: "gems" variable is out of scope.
}
Static Local Variables
We can extend the scope of a local variable in a function, such that it persists between function calls, by using the static
keyword.
int countingCrow() {
static int count{ 0 };
return ++count;
}
std::cout << countingCrow() << "\n"; // Prints: 1
std::cout << countingCrow() << "\n"; // Prints: 2
std::cout << countingCrow() << "\n"; // Prints: 3
Pass by Value
C++ functions are pass-by-value by default, meaning copies are made of the arguments passed to a function.
int increment(int number) {
number++;
return number;
}
void main() {
int a = 5;
int b = increment(a); // The value of "a" is copied to the "number" parameter.
std::cout << a << "\n"; // Remained 5
std::cout << b << "\n"; // 6
}
🎵 Note:
There is a performance “copy cost” for non-primitive types like arrays or objects.
Pass By Const Reference
To avoid the performance cost associated with pass-by-value we use const references for our function parameters. Passing a reference to a variable means no copy needs to be performed, the const
prevents the function from changing the referenced variable.
For example, let’s use a reference parameter to avoid the cost of copying a large object into a function.
void logMonster(const Monster& m) {
// Implementation details are unimportant.
}
☝️ Code assumes the Monster
class is defined elsewhere.
Pass by Reference
Changes made to non-const
reference parameters affect the variable passed into the function.
int mutator(int& number) {
number++; // Changes the value of the variable referenced by "number".
return number;
}
void main() {
int a = 5;
int b = mutator(a); // "a" and "b" become 6.
std::cout << a << "\n"; // 6
std::cout << b << "\n"; // 6
}
💡 Best Practice:
Prefer return
statements to using references to return data from functions.
In-Out Parameters
Keeping the above best practice in mind, the performance hit of copying large variables into and then out of a function can be avoided using in-out-parameters.
Non-const
reference parameters allow expensive to copy variables to be modified by a function:
void update(Monster& monster) {
// Updates the object referenced by "monster".
// Implementation unimportant.
}
Multiple Out Parameters
Resist the urge to use reference parameters as a way of returning multiple values from a function.
void calculateMovement(int time, int& xPosition, int& yPosition) {
// Implementation unimportant.
}
⏳ Wait For It:
It’s better to return a composite data type like a struct
, tuple
, pair
, etc.
Function Overloading
We can create different versions of the same named function that:
- Have different data types for the arguments.
- Have different numbers of arguments.
Imagine a debugging function called debugFormat()
defined in a few different ways:
void debugFormat() {
std::cout << "DEBUG\n";
}
void debugFormat(int number) {
std::cout << "DEBUG (int): " << number << "\n";
}
void debugFormat(double number) {
std::cout << "DEBUG (double): " << number << "\n";
}
void debugFormat(std::string label, double number) {
std::cout << label << " (double): " << number << "\n";
}
debugFormat(); // Outputs: DEBUG
debugFormat(42); // Outputs: DEBUG (int): 42
debugFormat(3.14); // Outputs: DEBUG (double): 3.14
debugFormat("WARNING", 3.14); // Outputs: WARNING (double): 3.14
Returning Different Types
There may also be instances when you overload a function with a different number/type of parameters and also change the return type.
int add(int num1, int num2) {
return num1 + num2;
}
double add(double num1, double num2) {
return num1 + num2;
}
🎵 Note:
Overloaded functions with different return types need not have identical function bodies.
⚡ Warning:
We cannot overload a function based only on a change in return type.