C++ Function Parameters
A function parameter is a variable listed inside a function’s parentheses that receives a value (an argument) when the function is called. How you declare a parameter — by value, by reference, or by pointer — determines whether the function works with a copy of the caller’s data or with the original data itself. Understanding this distinction is one of the most important skills in C++, because it affects both program correctness (does the function accidentally change something it shouldn’t?) and performance (are you copying a huge object every call?).
Overview / How it works
Every parameter in C++ has a passing mechanism that controls how data moves from the caller into the function:
- Pass by value — the function receives a copy of the argument. Changes made inside the function do not affect the original variable. This is the default in C++.
- Pass by reference — the parameter is declared with
&and becomes an alias for the original variable. No copy is made; changes inside the function are visible to the caller. - Pass by pointer — the parameter is a pointer (
*) holding the address of the argument. The function can read or modify the original data by dereferencing the pointer, and the caller must explicitly pass an address with&variable.
Internally, when you call a function, C++ builds a new stack frame for it. For pass-by-value parameters, the compiler copies the bytes of the argument into that stack frame — for an int this is cheap (4 bytes), but for a large struct, std::string, or std::vector it can mean copying thousands of bytes and even triggering heap allocations. For pass-by-reference parameters, no copy of the underlying data happens; the compiler typically implements the reference as a hidden pointer, so passing a reference is as cheap as passing a pointer, but syntactically it behaves like an alias — you use it exactly like the original variable, no dereferencing needed.
This is why the standard advice is: pass small, cheap-to-copy types (int, double, char, bool) by value, and pass large or expensive types (std::string, std::vector, custom classes) by reference — usually as const & if the function only needs to read the data.
Syntax
returnType functionName(paramType1 name1, paramType2 name2, ...) {
// function body uses name1, name2 like local variables
}
| Form | Declaration | Meaning |
|---|---|---|
| By value | void f(int n) |
Function gets a copy of the argument |
| By reference | void f(int &n) |
Function operates on the original variable |
| By const reference | void f(const int &n) |
Alias to the original, but the function cannot modify it |
| By pointer | void f(int *n) |
Function receives an address; must dereference with *n |
| Default argument | void f(int n = 10) |
Caller may omit the argument; 10 is used instead |
| Array parameter | void f(int arr[], int size) |
Array “decays” to a pointer; size must be passed separately |
A key rule for default arguments: once a parameter has a default value, every parameter after it must also have one. Defaults are also specified only once — typically in the function’s declaration (prototype or header), not repeated in the definition.
Examples
Example 1: Pass by value vs. pass by reference
#include <iostream>
using namespace std;
void incrementByValue(int n) {
n = n + 1;
cout << "Inside incrementByValue, n = " << n << endl;
}
void incrementByReference(int &n) {
n = n + 1;
cout << "Inside incrementByReference, n = " << n << endl;
}
int main() {
int a = 10;
incrementByValue(a);
cout << "After incrementByValue, a = " << a << endl;
incrementByReference(a);
cout << "After incrementByReference, a = " << a << endl;
return 0;
}
Output:
Inside incrementByValue, n = 11
After incrementByValue, a = 10
Inside incrementByReference, n = 11
After incrementByReference, a = 11
The first call copies a into n, so modifying n never touches a — after the call, a is still 10. The second call binds n as an alias for a itself, so modifying n modifies a directly — after the call, a becomes 11.
Example 2: Default arguments
#include <iostream>
using namespace std;
double calculatePrice(double basePrice, double taxRate = 0.08, double discount = 0.0) {
double taxed = basePrice * (1.0 + taxRate);
double finalPrice = taxed - discount;
return finalPrice;
}
int main() {
cout << "Price with defaults: " << calculatePrice(100.0) << endl;
cout << "Price with custom tax: " << calculatePrice(100.0, 0.05) << endl;
cout << "Price with tax and discount: " << calculatePrice(100.0, 0.05, 10.0) << endl;
return 0;
}
Output:
Price with defaults: 108
Price with custom tax: 105
Price with tax and discount: 95
The first call supplies only basePrice, so taxRate and discount fall back to their defaults (0.08 and 0.0). The second call overrides taxRate but still uses the default discount. The third call overrides both. Default arguments let callers opt into extra flexibility only when they need it, without forcing every call site to spell out every parameter.
Example 3: const reference and array parameters
#include <iostream>
#include <string>
#include <cctype>
using namespace std;
void printShout(const string &message) {
string shout = message;
for (char &c : shout) {
c = toupper(c);
}
cout << shout << "!!!" << endl;
}
double average(const int arr[], int size) {
int sum = 0;
for (int i = 0; i < size; i++) {
sum += arr[i];
}
return static_cast<double>(sum) / size;
}
int main() {
string greeting = "hello world";
printShout(greeting);
int scores[5] = {85, 90, 78, 92, 88};
cout << "Average score: " << average(scores, 5) << endl;
return 0;
}
Output:
HELLO WORLD!!!
Average score: 86.6
printShout takes const string &message — no copy of the caller’s string is made (efficient), and const guarantees the function cannot accidentally modify the caller’s original string. Inside, it makes its own local copy (shout) to transform. The average function shows array parameters: an array argument automatically “decays” into a pointer to its first element, so the array’s size is lost — that’s why size must be passed as a separate parameter.
How it works step by step / Under the hood
- When a function is called, the compiler allocates a new stack frame containing space for its parameters and local variables.
- For value parameters, the argument’s value is copied byte-by-byte (or via a copy constructor for classes) into that stack space. This copy is completely independent of the caller’s variable.
- For reference parameters, the compiler stores the address of the original variable (much like a pointer internally), but the syntax lets you use it as if it were the variable itself — no
*needed. - For pointer parameters, the address is copied by value (the pointer itself is copied), but since it points back to the original data, dereferencing it (
*ptr) reaches and can modify the original. - When the function returns, its entire stack frame — including any value-parameter copies — is destroyed. This is why you can never return a reference or pointer to a local value parameter and expect it to remain valid.
- Default arguments are resolved at compile time at the call site: the compiler literally inserts the default value into the generated call if the caller omitted it. This is different from runtime logic inside the function.
Common Mistakes
Mistake 1: Expecting pass-by-value to modify the caller’s variable
void doubleIt(int n) {
n = n * 2; // only changes the local copy
}
int main() {
int x = 5;
doubleIt(x);
// x is still 5 here — surprising to beginners
return 0;
}
Fix: use a reference parameter if the function is meant to modify the caller’s variable, or return the new value and assign it back.
void doubleIt(int &n) {
n = n * 2; // modifies the original
}
Mistake 2: Passing large objects by value unnecessarily
// Copies the entire vector every call — wasteful for large data
int sumAll(vector<int> numbers) {
int total = 0;
for (int n : numbers) total += n;
return total;
}
Fix: pass by const & since the function only reads the vector, avoiding the copy entirely.
int sumAll(const vector<int> &numbers) {
int total = 0;
for (int n : numbers) total += n;
return total;
}
Mistake 3: Placing a non-default parameter after a default one
// Compile error: default argument for taxRate, but discount has none after it
double calculatePrice(double basePrice, double taxRate = 0.08, double discount);
Fix: order parameters so every parameter with a default comes after all parameters without one.
Best Practices
- Pass small built-in types (
int,double,char,bool) by value — copying them is cheap and avoids aliasing bugs. - Pass large or expensive-to-copy types (
std::string,std::vector, custom classes) byconst &when the function only needs to read them. - Use a plain reference (
&, withoutconst) only when the function genuinely needs to modify the caller’s variable. - Prefer references over raw pointers for parameters when the argument can never legitimately be “no value” — references cannot be null, which eliminates a whole class of null-pointer bugs.
- Use pointers when the parameter is genuinely optional (the caller may pass
nullptr) or when working with C-style APIs or arrays. - Always pair array parameters with an explicit size parameter, since arrays decay to pointers and lose their length information.
- Put default arguments in the function’s declaration (usually in a header file), not duplicated in the definition.
- Keep parameter lists short and meaningful; if a function needs many related parameters, consider grouping them into a struct.
Practice Exercises
- Write a function
void swapValues(int &a, int &b)that swaps two integers using reference parameters, and call it frommainto swap two variables, printing their values before and after. - Write a function
double rectangleArea(double width, double height = 1.0)that returns the area of a rectangle, defaulting to a width-by-1 shape when no height is given. Call it both with and without the second argument. - Write a function
void appendExclaim(string &text)that appends"!"to a string passed by reference, and a second functionstring withExclaim(string text)that does the same but by value, returning a new string. Call both on the same original string and observe which one changes it.
Summary
- Pass-by-value copies the argument; changes inside the function do not affect the caller’s variable.
- Pass-by-reference (
&) creates an alias to the original variable; changes inside the function are visible to the caller. - Pass-by-pointer (
*) passes an address, letting the function read or modify the original data via dereferencing, and allows for optional (nullable) arguments. const &gives the efficiency of reference passing (no copy) with the safety of value passing (cannot modify the original).- Default arguments let callers omit trailing parameters, but every parameter after the first default must also have one.
- Arrays decay to pointers when passed as parameters, so their size must always be passed alongside them.
- Choosing the right passing mechanism affects both correctness (avoiding unwanted mutation) and performance (avoiding unnecessary copies).
