Universal Primer: C7 for Beginners
This chapter will be turned into something that feels clear instead of overwhelming. I’ll treat you as a serious beginner: you know basic C++ syntax, main(), variables, if, loops, etc., but functions and all the “extras” (headers, references, namespaces) feel fuzzy.
This chapter is basically:
“How to break programs into functions and organize them like a real programmer.”
I’ll go through the concepts in the same rough order as the chapter (but no exercises), with lots of why/why not examples.
1. What is a function and why use it?
Intuition
A function is like a mini-machine inside your program:
You give it inputs.
It does some work.
It optionally gives you an output.
In C++:
return_type function_name(parameter_list) {
// body
}
Example:
double calculate_mpg(double miles, double gallons) {
double mpg = miles / gallons;
return mpg;
}
Why we use functions:
Avoid repetition: if you compute MPG in 5 places, you don’t want 5 copies of the same code.
Readable structure:
calculate_mpg(...)is self-explanatory; reading formulas everywhere is not.Easy to test: you can test
calculate_mpgwith different inputs independently.Easier to fix: if the formula is wrong, fix it in one place instead of many.
Parameters vs arguments (important vocabulary)
Parameter: the variable in the function definition:
double calculate_mpg(double miles, double gallons) // ^^^^^^ ^^^^^^^ // parametersArgument: the actual value or variable you pass in the call:
double mpg = calculate_mpg(500.0, 14.0); // ^^^^^ ^^^^ // arguments
2. Defining and calling functions
2.1 Functions that return nothing: void
If a function doesn’t need to return a value, use void.
void display_title() {
cout << "Miles Per Gallon Calculator\n\n";
}
Call it like:
int main() {
display_title();
// ...
}
Why use this: even though it’s just one cout, if you display the same title in many places, or want to change the formatting later, having it in one function helps.
When it might be overkill: if you only use it once and it’s literally one cout, putting that code directly in main is fine. Don’t obsessively turn every one-liner into a function.
2.2 Functions that take parameters and return a value
Example: compute miles per gallon.
double calculate_mpg(double miles, double gallons) {
double mpg = miles / gallons;
// maybe round to 1 decimal place using <cmath> round
mpg = round(mpg * 10) / 10;
return mpg;
}
Usage:
int main() {
double miles, gallons;
cout << "Miles driven: ";
cin >> miles;
cout << "Gallons used: ";
cin >> gallons;
double mpg = calculate_mpg(miles, gallons);
cout << "MPG: " << mpg << endl;
}
Why do this instead of writing miles / gallons in main?
You may want the rounded version everywhere.
If your formula changes, you only fix it once.
You can test
calculate_mpgby itself with different values.
Mistake to avoid: calling the function and ignoring the return value when you actually need it:
calculate_mpg(miles, gallons); // ❌ does the work, but
// result is thrown away
Always capture the result if you need it:
double mpg = calculate_mpg(miles, gallons); // ✅
3. Function declarations (prototypes)
The compiler reads your file top to bottom. When it sees a call like:
int main() {
double mpg = calculate_mpg(500.0, 14.0);
}
it needs to know ahead of time:
What is
calculate_mpg?What does it return?
What parameters does it take?
You have two options:
Option 1: put function definitions before main
double calculate_mpg(double miles, double gallons) {
// body...
}
int main() {
double mpg = calculate_mpg(500.0, 14.0);
}
Compiler is happy; it saw the definition before the call.
Option 2: put prototypes at the top
This is what most real programs do, especially when main appears near the top.
// function declarations (prototypes)
void display_title();
double calculate_mpg(double miles, double gallons);
int main() {
display_title();
double mpg = calculate_mpg(500.0, 14.0);
}
// function definitions later
void display_title() {
cout << "Miles Per Gallon Calculator\n\n";
}
double calculate_mpg(double miles, double gallons) {
double mpg = miles / gallons;
return mpg;
}
A prototype is just:
return_type name(parameter_types...);
No body.
Ends with a semicolon.
Parameter names are optional in the prototype:
double calculate_mpg(double, double); // legal, but less
//readable
Why not skip prototypes altogether?
You could define everything before main, but as your files grow, you might want main near the top and helper functions below. Prototypes let you organize your code more flexibly.
4. Scope: local vs global variables
4.1 Local variables
Defined inside a function (including main):
int main() {
double tax = 0.0; // local to main
}
Only that function can see it.
It’s created when the function starts, destroyed when the function ends.
4.2 Global variables
Defined outside any function:
double tax = 0.0; // global variable
int main() {
// can read/write tax here
}
void calc_tax(double amount, double rate) {
// can read/write tax here too
}
Any function in that file (and possibly others) can read and modify it.
4.3 Why global variables are usually a bad idea
Imagine a shared Google Doc called tax that anyone on your team can edit. You open the doc and see a wrong number—but who changed it? You have to check everyone’s edits.
Code version:
double tax = 0.0; // global
void calc_tax(double amount, double tax_rate) {
tax = amount * tax_rate; // modifies global
}
void give_discount() {
tax = 0.0; // resets global for some reason
}
In a big program, you’d have:
Bugs that are hard to track (any function might have messed with
tax).Functions that secretly depend on
taxbeing in some particular state.
Better pattern:
Use local variables.
Pass values as parameters.
Return results instead of depending on globals.
Example:
double calc_tax(double amount, double tax_rate) {
return amount * tax_rate; // no globals
}
Now it’s clear: calc_tax depends only on its arguments.
4.4 Global constants are okay
If a value is truly constant for the whole program, a global const is fine:
const double TAX_RATE = 0.05; // OK as global
double calc_tax(double amount) {
return amount * TAX_RATE;
}
It can’t be changed by any function (compiler enforces that).
Many functions may need that constant.
This is much safer than a global
double taxthat changes.
4.5 Shadowing (name collision)
You can accidentally define a local variable with the same name as a global:
double tax = 0.0; // global
int main() {
double tax = 42.0; // shadows global
cout << tax << endl; // prints 42.0, local tax
}
Now inside main, tax refers to the local variable, not the global. This is very confusing and is almost always a bad idea.
5. Breaking a program into multiple functions (hierarchy)
The chapter uses a Temperature Converter as an example. Think about it as tasks:
Show a title and a menu.
Ask the user what they want to do.
If they choose Fahrenheit → Celsius:
get Fahrenheit
convert
show Celsius
If they choose Celsius → Fahrenheit:
get Celsius
convert
show Fahrenheit
You can organize this into functions like:
main()– overall driver.display_menu()– show menu.convert_temp()– handle one conversion, including user choice.to_celsius(fahrenheit)– formula.to_fahrenheit(celsius)– formula.
This is often drawn as a hierarchy chart:
maindisplay_menuconvert_tempto_celsiusto_fahrenheit
Why this is good:
Each function does one logical thing.
The formulas are isolated so if you need to change them, you know where to look.
mainreads like a high-level story, not a pile of details.
Bad alternative: put everything in main:
int main() {
// title
// print menu
// read choice
// if choice == 1, ask for F, convert here, print here
// else if choice == 2, ask for C, convert here, print here
// maybe loop, etc.
}
This quickly becomes a huge mess and is harder to maintain.
6. Default parameter values
Sometimes you want parameters that usually take some default value.
Example idea: a function computing future value of a savings plan:
double calculate_future_value(
double monthly_investment,
double yearly_interest_rate = 3.0, // default 3%
int years = 10 // default 10 years
) {
// ... compute ...
}
Now you can call:
// use all defaults except the first
double fv1 = calculate_future_value(100.0);
// override interest, keep years default
double fv2 = calculate_future_value(100.0, 4.0);
// override all
double fv3 = calculate_future_value(100.0, 4.0, 20);
Rules:
Parameters with defaults must go at the end of the parameter list:
double f(int x, int y = 10); // ok double f(int x = 5, int y); // not allowedIf you have both a prototype and a definition, put the default values in only one (usually the prototype). If you put defaults in both, the compiler complains (redefinition of default arguments).
When not to use defaults:
If omitting a parameter would be ambiguous or unsafe, don’t put a default. For example, if a function’s behavior drastically changes with one parameter, forcing the caller to always specify it might be clearer.
7. Function overloading
You can have multiple functions with the same name, as long as their parameter lists differ (in types and/or number of parameters). This is called overloading.
Example overloads for to_celsius:
double to_celsius(double fahrenheit); // 1
int to_celsius(int fahrenheit); // 2
double to_celsius(double fahrenheit, int decimals); // 3
These have different signatures:
Signature = function name + parameter types (and count).
The return type is NOT part of the signature.
So this is illegal:
double to_celsius(double fahrenheit);
int to_celsius(double fahrenheit); // ❌ same parameter list
The compiler wouldn’t know which one to call if you wrote to_celsius(98.6).
When overloading is nice:
Provide similar behavior for different input types:
double distance(double x1, double y1, double x2, double y2); double distance(double x1, double y1); // distance from originAdd a more advanced version (like
decimals) while keeping the simple one:double to_celsius(double fahrenheit); // basic double to_celsius(double fahrenheit, int decimals); // advanced
Then inside the advanced version, you can reuse the basic one:
double to_celsius(double fahrenheit, int decimals) {
double celsius = to_celsius(fahrenheit); // call simple version
// then round to 'decimals'
// ...
return celsius;
}
This avoids duplicating the formula.
8. Reference variables and reference parameters
This is a big concept. Let’s go slow.
8.1 Value vs reference variables
Normally, when you assign a variable to another, you copy the value:
double p1 = 54.50;
double p2 = p1; // copy
p1 = 57.50;
// p1 == 57.50, p2 == 54.50
They’re separate in memory.
A reference variable is declared with & and becomes an alias to another variable:
double p1 = 54.50;
double& p2 = p1; // p2 refers to p1
p1 = 57.50;
// p1 == 57.50, p2 == 57.50
p2 = 60.00;
// p1 == 60.00, p2 == 60.00
There is only one double in memory; both names point to the same spot.
You must initialize a reference when you declare it:
double& r; // illegal, must bind to something
8.2 Reference parameters in functions
Consider two versions of increase_price:
Version 1: value parameter
double increase_price(double price) {
price = price * 1.1; // increase by 10%
return price;
}
int main() {
double price = 54.50;
price = increase_price(price); // must assign the return
}
The function works on its own copy of
price.If you forget to capture the return value, nothing happens to the original variable.
Version 2: reference parameter
void increase_price(double& price) {
price = price * 1.1;
}
int main() {
double price = 54.50;
increase_price(price); // modifies price directly
}
The function’s
priceparameter refers to the caller’s variable.Any change inside the function changes the original.
When to use reference parameters:
When the function’s purpose is to modify the caller’s variable(s).
Examples:
void swap(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
When copying the parameter would be expensive (large objects), and:
you want to modify it →
T& paramor you want to avoid copying but not modify it →
const T& param
8.3 const reference parameters
If a function only needs to read a parameter (not change it), and that parameter might be big (like string or vector), pass it as a const reference:
void view_temps(const vector<double>& low,
const vector<double>& high);
Advantages:
&avoids copying large data.constguarantees you won’t modify them. If you try:low[0] = 10.0; // compile-time errorthe compiler will stop you.
This pattern is extremely common in C++.
9. Example: modifying a string with a reference parameter
Imagine a function that lowercases a string in place:
void to_lower(string& s) {
for (char& c : s) { // note char&!
c = tolower(c); // from <cctype>
}
}
Usage:
string name = "BJARNE";
to_lower(name);
cout << name << endl; // "bjarne"
Because s is a reference, the original name is modified.
Alternative approach (no side effects):
string lower_copy(string s) { // value parameter
for (char& c : s) {
c = tolower(c);
}
return s;
}
int main() {
string name = "BJARNE";
string small = lower_copy(name);
// name is unchanged, small is "bjarne"
}
This makes fewer “surprises” (no mutation), but may be slower for very large strings due to copying. For most beginner programs, clarity is more important than micro-efficiency.
10. Case-insensitive string comparison
The chapter shows a function like:
bool iequals(const string& s1, const string& s2) {
if (s1.size() != s2.size()) {
return false;
}
for (size_t i = 0; i < s1.size(); ++i) {
if (tolower(s1[i]) != tolower(s2[i])) {
return false;
}
}
return true;
}
Important details:
Parameters are
const string&→ efficient, read-only.Early
return falsewhen you detect a mismatch or different length.Returns
trueonly if all characters match ignoring case.
This is a good example of:
Using
constreferences.Classic loop logic.
Designing a small, focused function.
11. Larger example: Temperature Manager program
This is a more complex program that uses:
vectors
file I/O
reference parameters
const reference parameters
multiple functions
Data design
Two vectors:
vector<double> low_temps;
vector<double> high_temps;
Index i represents “Day i+1”
low_temps[i]= low temperature of that day.high_temps[i]= high temperature.
These are kept in sync.
File format (temps.txt)
Each line: low high
Example:
60 75
58 80
55 70
Functions
load_temps(vector<double>& low, vector<double>& high);reads from file into vectors (pass by reference because function modifies them).
save_temps(const vector<double>& low, const vector<double>& high);writes vectors back to file (const reference: doesn’t change them).
display_menu();view_temps(const vector<double>& low, const vector<double>& high);add_temps(vector<double>& low, vector<double>& high);gets new low/high from user, appends to vectors, and saves.
remove_temps(vector<double>& low, vector<double>& high);removes a day (index) from both vectors and saves.
Why this design is good:
Data is kept in vectors, not in globals that are modified randomly.
File I/O is centralized in
load_tempsandsave_temps.UI functions (
view_temps,add_temps,remove_temps) are separate from the main loop inmain.
12. Header files and implementation files
Real C++ programs are split into multiple files:
Header files (
.hor.hpp): contain declarations (function prototypes, type definitions, etc.).Source files (
.cpp): contain definitions (function bodies, actual code).
Example: temperature conversion helper.
temperature.h (header)
#ifndef TEMPERATURE_H
#define TEMPERATURE_H
double to_celsius(double fahrenheit);
double to_fahrenheit(double celsius);
#endif
This file tells the compiler:
“These functions exist somewhere.”
temperature.cpp (implementation)
#include "temperature.h"
double to_celsius(double fahrenheit) {
return (fahrenheit - 32.0) * 5.0 / 9.0;
}
double to_fahrenheit(double celsius) {
return celsius * 9.0 / 5.0 + 32.0;
}
Another file using it: main.cpp
#include <iostream>
#include "temperature.h"
using namespace std;
int main() {
double c = to_celsius(212.0);
cout << "Celsius: " << c << endl;
}
Why the #ifndef / #define / #endif?
This is an include guard:
#ifndef TEMPERATURE_H
#define TEMPERATURE_H
// contents
#endif
It prevents the header from being included multiple times in one build, which would cause multiple-definition errors.
What NOT to put in a header (for now)
Do not put
using namespace std;in a header.Do not define global variables in a header.
Do not define full functions (with bodies) in a header (unless they are
inlineor templates, which is advanced).
If you put full definitions in a header and include it in many .cpp files, you’ll get “multiple definition” linker errors.
13. Namespaces
Problem: What if two libraries both define a function called to_celsius?
Solution: put related functions inside a namespace.
temperature.h
#ifndef TEMPERATURE_H
#define TEMPERATURE_H
namespace temperature {
double to_celsius(double fahrenheit);
double to_fahrenheit(double celsius);
}
#endif
temperature.cpp
#include "temperature.h"
namespace temperature {
double to_celsius(double fahrenheit) {
return (fahrenheit - 32.0) * 5.0 / 9.0;
}
double to_fahrenheit(double celsius) {
return celsius * 9.0 / 5.0 + 32.0;
}
}
Using it
Option A: using namespace temperature;
#include "temperature.h"
using namespace std;
using namespace temperature;
int main() {
double c = to_celsius(212.0);
}
Option B: fully-qualified name:
#include "temperature.h"
using namespace std;
int main() {
double c = temperature::to_celsius(212.0);
}
The temperature:: is like the “last name”; to_celsius is the “first name”. Together they are unique.
14. A reusable console input library
The chapter builds a small reusable module for safe input: console.h and console.cpp in a console namespace.
console.h
Roughly:
#ifndef CONSOLE_H
#define CONSOLE_H
#include <string>
namespace console {
double get_double(std::string prompt,
double min = std::numeric_limits<double>::min(),
double max = std::numeric_limits<double>::max());
int get_int(std::string prompt,
int min = std::numeric_limits<int>::min(),
int max = std::numeric_limits<int>::max());
char get_char(std::string prompt, bool add_blank_line = true);
}
#endif
Key ideas inside console.cpp
get_double:prints prompt.
tries
cin >> value.if input fails (e.g., user types “abc”):
clears error flags.
ignores leftover chars until newline.
prints error and loops.
if succeeds, checks if value is within
[min, max].if not, prints error and loops.
get_intis similar, but forint.get_char:reads a single character (like
'y'or'n').optionally prints a blank line after.
Why this is great:
You don’t have to rewrite input-validation logic every time.
All the annoying
cin.fail(),cin.clear(),cin.ignore(...)stuff is wrapped up in one place.
15. Future Value Calculator program
This final example uses everything:
the
consolenamespace.functions + parameters.
loops and formatting.
The function
double calculate_future_value(double monthly_investment,
double yearly_interest_rate,
int years) {
double monthly_rate = yearly_interest_rate / 12 / 100;
int months = years * 12;
double future_value = 0.0;
for (int i = 0; i < months; ++i) {
future_value += monthly_investment;
future_value *= (1 + monthly_rate);
}
return future_value;
}
Pure calculation:
no I/O
all info comes from parameters
result is returned
main()
uses
console::get_doubleandconsole::get_intwith min/max ranges.calls
calculate_future_value.formats output.
asks user
Continue? (y/n):usingconsole::get_char.
This is a nicely structured program where:
all input handling is via
consolefunctions;all math is via
calculate_future_value;mainjust coordinates things.
Check Your Understanding (targeted, technical questions)
Your answers will tell me what to drill deeper on next.
Function basics
a) Write a function prototype for
double area_circle(double radius);and explain why a prototype is needed if the body goes belowmain.b) If you remove the prototype and keep the function body after
main, what exact kind of error do you expect from the compiler?
Parameters and overloading
a) Why is it legal to overload
to_celsius(int)andto_celsius(double), but illegal to overloaddouble to_celsius(double)andint to_celsius(double)?b) Given these overloads:
double to_celsius(double fahrenheit); int to_celsius(int fahrenheit);what happens when you call
to_celsius(100)? Which one is chosen and why?
Scope & globals
a) Suppose you have
double tax = 0.0;globally, and inmainyou do:double tax = 10.0; cout << tax << endl;What gets printed and why?
b) Describe one concrete bug scenario caused by using many non-
constglobal variables in a large program.
References
a) In memory terms, what is the difference between:
double a = 5.0; double b = a; double& c = a;How many doubles are actually stored, and how do
a,b, andcrelate to them?b) Why is
const string& soften preferred overstring sfor function parameters that only need to read the string?
Headers & namespaces
a) Write a minimal header file
math_utils.hwith:an include guard,
a
math_utilsnamespace,function declarations for
double square(double);anddouble cube(double);.
b) Show how you would call
square(3.5)frommainwithout writingusing namespace math_utils;.
If you answer these, you’ll see which subtopics—functions, scope, references, headers, etc.—need more focused, example-heavy explanations before we move to the next chapter.