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. Important point:

Each symbol (name, variable, etc.) can only be associated with one address, which is why this is illegal:

int X;    // bind X to some address                  
float X;  // error, X is already bound to an address 
double X; // error, X is already bound to an address 
You can only associate one address with one name. However, as we'll soon see, you can associate multiple names with one address.

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 existing 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 (that already exists), requires no storage
              //    we call this alias a reference
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 be associated with 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 pointer 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 &ri = 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
Another example:
Given this code:
int i = 10;

int &r1 = i;
int &r2 = i;
int &r3 = i;
int &r4 = i;
int &r5 = i;
Diagram:

In fact, we could even do this:

Given this code:
int r3 = 10;

int &r4 = r3;
int &r2 = r4;
int &r5 = r4;
int &r1 = r2;
int &i = r5;

// j is NOT a
// reference
int j = r5;
Same Diagram:



There is NO WAY to determine which was the "original" integer and which are references. They ALL refer to the same memory location (200).

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.

In the examples above, i IS ri and ri IS i. They can forever be used interchangeably and there is no way that the system can distinguish between the two names. In fact, the compiler discards the names and just uses the address (e.g. 200) in their place.

Reference Parameters

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

The true power of references will become apparent when we learn about operator overloading.

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)              */
/* Note that this is NOT the address-of operator (yes, this is confusing) */
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

This is the assembly code that was generated by the compiler. The two functions generate the same assembly code showing that references are merely pointers behind-the-scenes with the compiler doing all of the dereferencing and pointer manipulation.

swappswapr
_Z5swappPiS_:
  endbr32
  pushl %ebx
  movl  8(%esp), %edx
  movl  12(%esp), %eax
  movl  (%edx), %ecx
  movl  (%eax), %ebx
  movl  %ebx, (%edx)
  movl  %ecx, (%eax)
  popl  %ebx
  ret
_Z5swaprRiS_:
  endbr32
  pushl %ebx
  movl  8(%esp), %edx
  movl  12(%esp), %eax
  movl  (%edx), %ecx
  movl  (%eax), %ebx
  movl  %ebx, (%edx)
  movl  %ecx, (%eax)
  popl  %ebx
  ret

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:
  // Note that no error checking is done.  
void calculate_quadratic(float a, float b, float c, float &root1, float &root2)
{
  float discriminant = b * b - 4 * a * 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
This seems like a lot of duplication (and it is), especially if the function was a lot bigger than this. We have a few choices to make:
  1. Change the original function to print each number on its own line.
  2. Create a second function (as above) that is mostly the same.
  3. Add a default parameter to the function.
Adding a default parameter to the original 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, false); // Last argument supplied by the programmer. (explicit)
print_array(a, size);        // Last argument supplied by the compiler. (implicit)

  // Calls with (a, size, true)
print_array(a, size, true); // Must be supplied by the programmer. (explicit)
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;
By the way, had the Inc function be defined as returning void, it would require this:
int i = 10;
Inc(i, 1);                   // increment
std::cout << i << std::endl; // use
Inc(i, 1);                   // increment
std::cout << i << std::endl; // use
Inc(i, 2);                   // increment
std::cout << i << std::endl; // use
// etc...
This is because a function that doesn't return anything (void) cannot be used in an expression.

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

How about this? Why does this work?
i = 10;
std::cout << Inc(Inc(Inc(i, 5))) << std::endl;


Without the return reference:
main.cpp:282:27: error: cannot bind non-const lvalue reference of type 'int&'' to an rvalue of type 'int'
   std::cout << Inc(Inc(Inc(i, 5))) << std::endl;
                        ~~~^~~~~~
main.cpp:263:5: note:   initializing argument 1 of 'int Inc(int&, int)'
 int Inc(int& value, int amount = 1)
     ^~~
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;
Answer:
Undefined:
g++  Bor/MS Clang
18    18      11
17    17      12
16    16      14
14    14      18

Older g++ used to give 18 for all

Note:

With C++17, all compilers are now guaranteed to give the same output, which is the output that Clang gives:
11
12
14
18

See this for details.

Given these two lines of C++ code:

Inc(i, 5); // The user provides the 5
Inc(i);    // The compiler provides the 1
This is what the assembly code might look like:
  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

  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
You can have multiple default parameters and they must default right to left:

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.


The solution: Overloaded functions:

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) functions and virtual functions. These types of functions will be discussed later.

Some Issues with References (Somewhat advanced)

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 an unnamed temporary by the compiler)

int &r3 = i;           // Ok
int &r4 = i + j;       // Error, i + j is in a temporary (maybe a register on the CPU)
const int &r5 = i + j; // Ok, i + j is in a temp but r5 is a reference to 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 (or compiler) 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?