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:

  1. Avoid repetition: if you compute MPG in 5 places, you don’t want 5 copies of the same code.

  2. Readable structure: calculate_mpg(...) is self-explanatory; reading formulas everywhere is not.

  3. Easy to test: you can test calculate_mpg with different inputs independently.

  4. 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)
    //                   ^^^^^^       ^^^^^^^
    //                   parameters
    
  • Argument: 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_mpg by 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 tax being 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 tax that 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:

  1. Show a title and a menu.

  2. Ask the user what they want to do.

  3. If they choose Fahrenheit → Celsius:

    • get Fahrenheit

    • convert

    • show Celsius

  4. 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:

  • main

    • display_menu

    • convert_temp

      • to_celsius

      • to_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.

  • main reads 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:

  1. 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 allowed
    
  2. If 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 origin
    
  • Add 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 price parameter 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& param

    • or 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.

  • const guarantees you won’t modify them. If you try:

    low[0] = 10.0;   // compile-time error
    

    the 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 false when you detect a mismatch or different length.

  • Returns true only if all characters match ignoring case.

This is a good example of:

  • Using const references.

  • 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_temps and save_temps.

  • UI functions (view_temps, add_temps, remove_temps) are separate from the main loop in main.


12. Header files and implementation files

Real C++ programs are split into multiple files:

  • Header files (.h or .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 inline or 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_int is similar, but for int.

  • 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 console namespace.

  • 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_double and console::get_int with min/max ranges.

  • calls calculate_future_value.

  • formats output.

  • asks user Continue? (y/n): using console::get_char.

This is a nicely structured program where:

  • all input handling is via console functions;

  • all math is via calculate_future_value;

  • main just coordinates things.


Check Your Understanding (targeted, technical questions)

Your answers will tell me what to drill deeper on next.

  1. Function basics

    • a) Write a function prototype for double area_circle(double radius); and explain why a prototype is needed if the body goes below main.

    • b) If you remove the prototype and keep the function body after main, what exact kind of error do you expect from the compiler?

  2. Parameters and overloading

    • a) Why is it legal to overload to_celsius(int) and to_celsius(double), but illegal to overload double to_celsius(double) and int 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?

  3. Scope & globals

    • a) Suppose you have double tax = 0.0; globally, and in main you do:

      double tax = 10.0;
      cout << tax << endl;
      

      What gets printed and why?

    • b) Describe one concrete bug scenario caused by using many non-const global variables in a large program.

  4. 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, and c relate to them?

    • b) Why is const string& s often preferred over string s for function parameters that only need to read the string?

  5. Headers & namespaces

    • a) Write a minimal header file math_utils.h with:

      • an include guard,

      • a math_utils namespace,

      • function declarations for double square(double); and double cube(double);.

    • b) Show how you would call square(3.5) from main without writing using 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.