C++ Functions
A function is a named, reusable block of code that performs a specific task. Instead of writing the same instructions over and over, you write them once inside a function and then call that function whenever you need it. Functions are the primary tool for breaking a large program into smaller, manageable, and testable pieces, and they are fundamental to how every C++ program — including main() itself — is structured.
Overview / How Functions Work
Every C++ program has at least one function: main(), which is where execution begins. As programs grow, you split logic into additional functions so each piece of code has a single, clear responsibility. A function has a name, a list of parameters (inputs it accepts), a return type (the type of value it produces), and a body (the statements it executes).
Internally, when you call a function, the compiler generates code that: pushes the function’s arguments and a return address onto the call stack, transfers control (jumps) to the function’s first instruction, executes the function body using its own local stack frame for local variables and parameters, and then — when a return statement is reached or the function ends — pops that stack frame off, copies the return value (if any) back to the caller, and jumps back to the address right after the call. This is why local variables inside a function only exist while that function is running: once the stack frame is popped, that memory is reclaimed and reused by whatever the program does next.
Because parameters are (by default) passed by value, the function receives a copy of each argument. Changes made to a parameter inside the function do not affect the original variable in the caller — unless you explicitly pass by reference (using &), which lets the function operate on the original variable rather than a copy. This distinction is one of the most important things to understand about C++ functions and is covered in more detail below.
C++ is a statically typed, compiled language, so every function must have its types known at compile time: the compiler needs to know a function’s return type and parameter types before it can generate correct machine code for a call. This is why functions are typically declared (a prototype, telling the compiler the function’s signature) before they are defined (the actual body), especially when the definition appears later in the file or in a different file entirely.
Syntax
returnType functionName(parameterType1 paramName1, parameterType2 paramName2, ...) {
// function body
return value; // omitted if returnType is void
}
| Part | Meaning |
|---|---|
returnType |
The type of value the function sends back to the caller (e.g. int, double, std::string). Use void if the function returns nothing. |
functionName |
An identifier used to call the function. Should be a descriptive verb-like name, e.g. calculateArea. |
parameterType paramName |
Each input the function accepts, with its type and a local name used inside the body. A function can take zero or more parameters. |
return value; |
Ends the function and sends value back to the caller. Must match (or convert to) returnType. Omitted entirely for void functions (a bare return; is optional). |
A function must be declared (prototyped) before it is used if its full definition comes later:
returnType functionName(parameterType1, parameterType2); // declaration, ends with ;
Examples
Example 1: A Simple Function
#include <iostream>
using namespace std;
int square(int number) {
return number * number;
}
int main() {
int result = square(7);
cout << "7 squared is " << result << endl;
return 0;
}
Output:
7 squared is 49
Here square takes one int parameter, computes its square, and returns the result. In main(), calling square(7) copies the value 7 into the parameter number, runs the function body, and returns 49, which is stored in result.
Example 2: Multiple Parameters and a Void Function
#include <iostream>
using namespace std;
double calculateAverage(double a, double b, double c) {
return (a + b + c) / 3.0;
}
void printReport(string studentName, double average) {
cout << "Student: " << studentName << endl;
cout << "Average score: " << average << endl;
}
int main() {
double avg = calculateAverage(85.5, 90.0, 78.5);
printReport("Alice", avg);
return 0;
}
Output:
Student: Alice
Average score: 84.6667
calculateAverage returns a double that we store in avg. printReport is a void function — it performs an action (printing) instead of returning a value, so it has no return statement carrying a value.
Example 3: Pass by Reference, Default Arguments, and Overloading
#include <iostream>
using namespace std;
// Pass by reference: modifies the caller's original variables
void doubleValue(int &value) {
value = value * 2;
}
// Default argument: taxRate defaults to 0.08 if not provided
double finalPrice(double price, double taxRate = 0.08) {
return price + (price * taxRate);
}
// Function overloading: same name, different parameter types
int add(int a, int b) {
return a + b;
}
double add(double a, double b) {
return a + b;
}
int main() {
int x = 10;
doubleValue(x);
cout << "x after doubleValue: " << x << endl;
cout << "Price with default tax: " << finalPrice(100.0) << endl;
cout << "Price with custom tax: " << finalPrice(100.0, 0.05) << endl;
cout << "add(int): " << add(2, 3) << endl;
cout << "add(double): " << add(2.5, 3.5) << endl;
return 0;
}
Output:
x after doubleValue: 20
Price with default tax: 108
Price with custom tax: 105
add(int): 5
add(double): 6
This example shows three important features. doubleValue takes its parameter as a reference (int &value), so it modifies the original x in main() rather than a copy. finalPrice has a default argument, so calling it with one argument uses 0.08 automatically, while supplying a second argument overrides it. Finally, add is overloaded: two functions share the name add but differ in parameter types, and the compiler picks the correct one based on the arguments you pass.
Under the Hood: What Happens on a Function Call
- Argument evaluation: The compiler evaluates each argument expression and prepares its value.
- Stack frame setup: A new stack frame is pushed for the called function, storing its parameters (copies, for pass-by-value) and local variables, along with the return address (where to resume in the caller).
- Execution: The function body runs statement by statement using this stack frame’s memory.
- Return: When
returnexecutes (or the closing brace of avoidfunction is reached), the return value (if any) is copied to a location the caller can access, the function’s stack frame is popped (destroying its local variables), and control jumps back to the return address. - Resumption: The caller resumes execution, using the returned value if it captured one.
This stack-based mechanism is also why deep or infinite recursion causes a stack overflow: each recursive call adds another frame to the stack, and if frames are never popped (because the base case is never reached), the stack runs out of memory.
Common Mistakes
Mistake 1: Forgetting to Return a Value
// Wrong: declared to return int, but no return statement on all paths
int absoluteValue(int n) {
if (n < 0) {
return -n;
}
// missing return for the case where n >= 0
}
This compiles with a warning (and undefined behavior if the missing path is taken) because not every code path returns a value. Always ensure every possible path through a non-void function reaches a return:
int absoluteValue(int n) {
if (n < 0) {
return -n;
}
return n;
}
Mistake 2: Expecting Pass-by-Value to Modify the Original
// Wrong: value is a copy, so the original is untouched
void increment(int value) {
value = value + 1;
}
int main() {
int count = 5;
increment(count);
// count is still 5 here, not 6 -- surprising to beginners
return 0;
}
Because value is passed by value, increment only modifies its own local copy. To actually change the caller’s variable, pass by reference:
void increment(int &value) {
value = value + 1;
}
Mistake 3: Calling a Function Before It’s Declared
// Wrong: greet() is used before the compiler has seen its declaration
int main() {
greet(); // error: 'greet' was not declared in this scope
return 0;
}
void greet() {
// ...
}
C++ compiles top to bottom and needs to know a function’s signature before it is called. Fix this by adding a prototype before main(), or by defining greet() before main():
void greet(); // declaration
int main() {
greet(); // now valid
return 0;
}
void greet() {
// definition
}
Best Practices
- Give each function a single, clear responsibility — if you struggle to name it without using “and,” split it into two functions.
- Use descriptive, verb-based names, like
calculateTotalorisValidEmail, so calls read like plain English. - Pass large objects (like
std::stringorstd::vector) byconst&to avoid expensive copies when you don’t need to modify them. - Only use reference parameters (
&withoutconst) when the function is genuinely meant to modify the caller’s variable; otherwise it’s confusing. - Keep functions short. If a function scrolls past a screen, consider breaking it into smaller helper functions.
- Declare function prototypes in header files for multi-file projects so functions can be used before their full definition is compiled.
- Avoid overusing default arguments and overloading together, as it can make it unclear which version of a function actually runs.
Practice Exercises
- Exercise 1: Write a function
int maxOfThree(int a, int b, int c)that returns the largest of three integers. Call it frommain()with a few different sets of numbers and print the results. - Exercise 2: Write a
voidfunctionswapValues(int &a, int &b)that swaps two integers using pass-by-reference. Demonstrate inmain()that the original variables are swapped after the call. - Exercise 3: Write an overloaded function
area: one versiondouble area(double radius)that computes a circle’s area, and anotherdouble area(double length, double width)that computes a rectangle’s area. Call both frommain()and print the results.
Summary
- A function is a named, reusable block of code with a return type, a name, parameters, and a body.
- Function calls use the call stack: arguments and local variables live in a stack frame that is created on call and destroyed on return.
- Parameters are passed by value by default (the function gets a copy); use references (
&) to let a function modify the caller’s original variables. voidfunctions perform actions without returning a value; other functions must return a value on every code path.- Default arguments let you omit trailing parameters, and function overloading lets multiple functions share a name as long as their parameter lists differ.
- Functions must be declared before use; prototypes let you call a function before its full definition appears.
