Functions, Part 2

Brief Pointer and Memory Review

Here's a brief review of pointers and memory:

We can declare pointer variables easily:

void foo()
{
  int i;  /* i can only store integer values            */
          /* The value of i is undefined at this point  */

  int *p; /* p can only store the address of an integer */
          /* The value of p is undefined at this point  */

  p = &i; /* The value of p is now the address of i     */
  i = 10; /* The value of i is now 10                   */
}
This is the notation that will be used when talking about variables in memory:

Visualizing the code above:

After declarations for i and pAfter assignment to pAfter assignment to i



One important thing to realize is that once you name a memory location, that name can not be used for another memory location (in the same scope). In other words, once you bind a name to a memory location, you can't unbind it:
int i;   /* i is the name of this memory location                        */
float i; /* i is now attempting to "rename" this memory location (not legal) */ 
In the diagrams above, you can see that it is possible to modify i's value in two different ways.

References

A reference can be thought of as an alias for another variable (i.e. a memory location). This means that a reference, unlike a pointer, does not take up any additional memory. A reference is just another name for an object. An example with a diagram will make it clearer:
int i = 10;   // i represents an address, requires 4 bytes, holds the value 10 
int *pi = &i; // pi is a pointer, requires 4 or 8 bytes bytes, holds the address of i
int &ri = i;  // ri is an alias (another name) for i, requires no storage
              //    we call this alias a reference
Diagram:

You can see that i and ri do, in fact, represent the same piece of memory:
std::cout << " i is " << i << std::endl;
std::cout << "ri is " << ri << std::endl;

std::cout << "address of  i is " << &i << std::endl;
std::cout << "address of ri is " << &ri << std::endl;
Output:
 i is 40
ri is 40
address of  i is 0012FE00
address of ri is 0012FE00
Compare that with pi, which is a separate entity in the program:
std::cout << "pi is " << pi << std::endl;
std::cout << "*pi is " << *pi << std::endl;
std::cout << "address of pi is " << &pi << std::endl;
Output:
pi is 0012FE00
*pi is 40
address of pi is 0012FDF4
When you declare a reference, you must initialize it. You can't have any unbound references (or variables for that matter). In this respect, it is much like a constant pointer that must point to something when it is declared:
int i;         // i is bound to a memory location by the compiler
int &r1 = i;   // r1 is bound to the same memory location as i
int &r2;       // error: r2 is not bound to anything

int * const p1 = &i;  // Ok, p1 points to i
int * const p2;       // error, p2 must be initialized
p1 = &i;               // error, p1 is a constant so you can't modify it
The error message for the uninitialized reference will be something like this:
error: 'r2' declared as reference but not initialized
Of course, just like when you first learned about pointers, your response was: "Yeah, so what?"

Recall this example:

int i;         // i is bound to a memory location by the compiler
int &r1 = i;   // r1 is bound to the same memory location as i
The results of the definitions above are 100% identical to this:
int ri;       // ri is bound to a memory location by the compiler
int &i = ri;  // i is bound to the same memory location as ri
Note:

In the examples above, there is absolutely, positively, no difference between i and ri. None. Nada. Zero. Zip. They are just two different names for the same thing. Please remember that. In fact, at runtime, there is no way to tell if the program was using i or ri when accessing the integer. They are the SAME thing.

Reference Parameters

We don't often create a reference (alias) for another variable since it rarely provides any benefit. The real benefit comes from using references as parameters to functions.

We know that, by default, parameters are passed by value. If we want the function to modify the parameters, we need to pass the address of the data we want modified. The scanf function is a classic example:

int a, b, c;
scanf("%d%d%d", &a, &b, &c); // scanf can modify a, b, and c
Another classic example is the swap function. This function is "broken", because it passes by value.

int main()
{
  int x = 10;
  int y = 20;

  printf("Before: x = %i, y = %i\n", x, y);
  swapv(x, y);
  printf(" After: x = %i, y = %i\n", x, y);

  return 0;
}
/* Pass the parameters by value */
void swapv(int a, int b)
{
  int temp = a; /* Save a for later      */
  a = b;        /* a gets value of b     */
  b = temp;     /* b gets old value of a */
}


Output:
Before swap: x = 10, y = 20
 After swap: x = 10, y = 20

We fixed this in C by passing the address:

int main()
{
  int x = 10;
  int y = 20;

  printf("Before swap: x = %i, y = %i\n", x, y);
  swapp(&x, &y);
  printf(" After swap: x = %i, y = %i\n", x, y);

  return 0;
}
/* Pass the parameters by address (need to dereference now) */
void swapp(int *a, int *b)
{
  int temp = *a; /* Save a for later      */
  *a = *b;       /* a gets value of b     */
  *b = temp;     /* b gets old value of a */
}


Output:
Before swap: x = 10, y = 20
 After swap: x = 20, y = 10

In C++, we can pass by reference, which acts kind of like pass by address. The only thing that has changed between this and the first pass-by-value function is the & in the parameters to the swap function.

int main()
{
  int x = 10;
  int y = 20;

  printf("Before: x = %i, y = %i\n", x, y);
  swapr(x, y);
  printf(" After: x = %i, y = %i\n", x, y);

  return 0;
}
/* Pass the parameters by reference (no need to dereference) */
void swapr(int &a, int &b)
{
  int temp = a; /* Save a for later      */
  a = b;        /* a gets value of b     */
  b = temp;     /* b gets old value of a */
}


Output:
Before swap: x = 10, y = 20
 After swap: x = 20, y = 10

Note: When you pass a parameter by reference, you are actually passing an address to the function. Roughly speaking, you get pass-by-address semantics with pass-by-value syntax. The compiler is doing all of the necessary dereferencing for you behind the scenes.


This example allows us to return two values from a function. We'll be evaluating the quadratic formula:

      
The code:
// Helper function
float calculate_discriminant(float a, float b, float c)
{
  return b * b - 4 * a * c;
}

void calculate_quadratic(float a, float b, float c, float &root1, float &root2)
{
  float discriminant = calculate_discriminant(a, b, c);

  float pos_numerator = -b + std::sqrt(discriminant);
  float neg_numerator = -b - std::sqrt(discriminant);
  float denominator = 2 * a;

    // root1 and root2 were passed in as references
  root1 = pos_numerator / denominator;
  root2 = neg_numerator / denominator;
}
Calling the function:
float a = 1.0f, b = 4.0f, c = 2.0f;
float root1, root2; // These are NOT references!

  // Calculate both roots (root1 and root2 are passed by reference)
calculate_quadratic(a, b, c, root1, root2);

std::cout << "a = " << a << ", b = " << b;
std::cout << ", c = " << c << std::endl;
std::cout << "root1 = " << root1 << std::endl;
std::cout << "root2 = " << root2 << std::endl;

Output:

a = 1, b = 4, c = 2
root1 = -0.585786
root2 = -3.41421


Another example:

Using values:

/* Assumes there is at least one element in the array  */
int find_largest1(int a[], int size)
{
  int max = a[0]; /* assume 1st is largest */

  for (int i = 1; i < size; i++)
    if (a[i] > max) 
      max = a[i]; /* found a larger one */

  return max;     /* max is the largest */
}
Call the function:
int a[] = {4, 5, 3, 9, 5, 2, 7, 6};
int size = sizeof(a) / sizeof(*a);

int largest = find_largest1(a, size);
std::cout << "Largest value is " << largest << std::endl;


Using pointers:

/* Assumes there is at least one element in the array  */
int* find_largest2(int a[], int size)
{
  int max = 0;  /* assume 1st is largest */

  for (int i = 1; i < size; i++)
    if (a[i] > a[max]) 
      max = i;   /* found a larger one */

  return &a[max]; /* return the largest */
}
Calling the function:
  // Have to dereference the returned pointer
largest = *find_largest2(a, size);
std::cout << "Largest value is " << largest << std::endl;


Using references:

/* Assumes there is at least one element in the array  */
int& find_largest3(int a[], int size)
{
  int max = 0;  /* assume 1st is largest */

  for (int i = 1; i < size; i++)
    if (a[i] > a[max]) 
      max = i;   /* found a larger one */

  return a[max]; /* return the largest */
}
Calling the function:
largest = find_largest3(a, size);
std::cout << "Largest value is " << largest << std::endl;
Notes:

Default Parameters

Examples:
FunctionCalling function
void print_array(int a[], int size)
{
  for (int i = 0; i < size; i++)
  {
    std::cout << a[i];
    if (i < size - 1)
      std::cout << ", ";
  }
  std::cout << std::endl;
}
int a[] = {4, 5, 3, 9, 5, 2, 7, 6};
int size = sizeof(a) / sizeof(*a);

print_array(a, size);





Output:
4, 5, 3, 9, 5, 2, 7, 6
Change the formatting:
FunctionCalling function
void print_array2(int a[], int size)
{
  for (int i = 0; i < size; i++)
    std::cout << a[i] << std::endl;
}
int a[] = {4, 5, 3, 9, 5, 2, 7, 6};
int size = sizeof(a) / sizeof(*a);

print_array2(a, size);

Output:
4
5
3
9
5
2
7
6
Adding a default parameter to the function:
void print_array(int a[], int size, bool newlines = false)
{
  for (int i = 0; i < size; i++)
  {
    std::cout << a[i];
    if (i < size - 1)
      if (newlines)
        std::cout << std::endl;
      else
        std::cout << ", ";
  }
  std::cout << std::endl;
}
Calling the function with/without the default parameter:
  // Calls with (a, size, false)
print_array(a, size); 
print_array(a, size, false); 

  // Calls with (a, size, true)
print_array(a, size, true);
Another example:
FunctionCalling code
int& Inc(int& value, int amount)
{
  value += amount;
  return value;
}
int i = 10;
std::cout << Inc(i, 1) << std::endl;
std::cout << Inc(i, 1) << std::endl;
std::cout << Inc(i, 2) << std::endl;
std::cout << Inc(i, 4) << std::endl;
std::cout << Inc(i, 5) << std::endl;
Using default parameters:
FunctionCalling code
int& Inc(int& value, int amount = 1)
{
  value += amount;
  return value;
}
int i = 10;
std::cout << Inc(i) << std::endl;
std::cout << Inc(i) << std::endl;
std::cout << Inc(i, 2) << std::endl;
std::cout << Inc(i, 4) << std::endl;
std::cout << Inc(i, 5) << std::endl;
Output:
11
12
14
18
23
Bonus Question: What is the output from this?
int i = 10;

  // This is a single statement
std::cout << Inc(i) << std::endl
          << Inc(i) << std::endl
          << Inc(i, 2) << std::endl
          << Inc(i, 4) << std::endl;
Given these two lines of C++ code:
Inc(i);
Inc(i, 5);
This is what the assembly code might look like:
  Inc(i);
0041E7F8  push        1             ; push 1 on the stack
0041E7FA  lea         eax,[i]       ; get address of i
0041E7FD  push        eax           ; and push it on the stack
0041E7FE  call        Inc (41BC0Dh) ; call the function
0041E803  add         esp,8         ; remove parameters from stack

  Inc(i, 5);
0041E806  push        5             ; push 5 on the stack
0041E808  lea         eax,[i]       ; get address of i
0041E80B  push        eax           ; and push it on the stack
0041E80C  call        Inc (41BC0Dh) ; call the function
0041E811  add         esp,8         ; remove parameters from stack
You can have multiple default parameters:

void foo(int a, int b, int c = 10);

foo(1, 2);    // foo(1, 2, 10)
foo(1, 2, 9); // foo(1, 2, 9)
void foo(int a, int b = 8, int c = 10);

foo(1);       // foo(1, 8, 10)
foo(1, 2);    // foo(1, 2, 10)
foo(1, 2, 9); // foo(1, 2, 9)
void foo(int a = 5, int b = 8, int c = 10);

foo();        // foo(5, 8, 10)
foo(1);       // foo(1, 8, 10)
foo(1, 2);    // foo(1, 2, 10)
foo(1, 2, 9); // foo(1, 2, 9)

These two functions are illegal because of the ordering of the default parameters:

void foo(int a, int b = 8, int c);
void foo(int a = 5, int b, int c = 10);
Notes:

Overloaded Functions

int cube(int n)
{
  return n * n * n;
}
int i = 8;
long l = 50L;
float f = 2.5F;
double d = 3.14;

  // Works fine: 512
std::cout << cube(i) << std::endl;

  // May or may not work: 125000
std::cout << cube(l) << std::endl;

  // Not quite what we want: 8
std::cout << cube(f) << std::endl;

  // Not quite what we want: 27
std::cout << cube(d) << std::endl;
First attempt, "old skool" fix in C:
int cube_int(int n)
{
  return n * n * n;
}

float cube_float(float n)
{
  return n * n * n;
}
double cube_double(double n)
{
  return n * n * n;
}

long cube_long(long n)
{
  return n * n * n;
}
It will work as expected:
  // Works fine: 512
std::cout << cube_int(i) << std::endl;

  // Works fine: 125000
std::cout << cube_long(l) << std::endl;

  // Works fine: 15.625
std::cout << cube_float(f) << std::endl;

  // Works fine: 30.9591
std::cout << cube_double(d) << std::endl;
This quickly becomes tedious and unmanageable as we write other functions to handle other types such as unsigned int, unsigned long, char, as well as user-defined types that might come along.


Using overloaded functions in C++:

int cube(int n)
{
  return n * n * n;
}

float cube(float n)
{
  return n * n * n;
}
double cube(double n)
{
  return n * n * n;
}

long cube(long n)
{
  return n * n * n;
}
It will also work as expected without the user needing to choose the right function:
  // Works fine, calls cube(int): 512
std::cout << cube(i) << std::endl;

  // Works fine, calls cube(long): 125000
std::cout << cube(l) << std::endl;

  // Works fine, calls cube(float): 15.625
std::cout << cube(f) << std::endl;

  // Works fine, calls cube(double): 30.9591
std::cout << cube(d) << std::endl;
Now, if we decide we need to handle another data type, we simply overload the cube function to handle the new type. The users (clients) of our code have no idea that we implement the cube function as separate functions.

More example uses:

int i = cube(2);           // calls cube(int), i is 8
long l = cube(100L);       // calls cube(long), l is 1000000L
float f = cube(2.5f);      // calls cube(float), f is 15.625f
double d = cube(2.34e25);  // calls cube(double), d is 1.2812904e+76
Notes

Technical note: For normal functions, the return type is not part of the signature. The exceptions to this rule are non-normal functions which include template-generated (with specializations) and virtual functions. These types of functions will be discussed later.

Some Issues with References

Some more subtle issues with references, mostly pertaining to constant references:
int i = 10;
int j = 20;

int &r1 = 5;        // Error. How do you change '5'? 
const int &r2 = 5; // Ok, r2 is const (5 is put into a temporary by the compiler)

int &r3 = i;            // Ok
int &r4 = i + j;        // Error, i + j is in temporary (probably a register on the CPU)
const int &r5 = i + j; // Ok, i + j is in temp but r5 is const
We will see more of these issues when dealing with functions and reference parameters.

Q: Why would you use pass-by-reference instead of pass-by-address?
A: When we start working with classes and objects, we'll see that references are much more natural than pointers.

Also, with references, the caller can't tell the difference between passing by value and passing by reference. This allows the caller to always use the same syntax and let the function decide the optimal way to receive the data.


Understanding the Big Picture™

What problems are solved by

  1. references?
  2. reference parameters?
  3. default parameters?
  4. overloaded functions?