Toggle Menu

Functions


Functions are the building blocks of programs. The 3 advantages of functions are:
  1. Divide & Conquer
  2. Readability
  3. Re-usability
There are 2 types of functions:
  1. Returns a value - function returns a value (can be used as a value). Must include a return statement
  2. Does not return a value (void) - just performs an action; do not return a value. e.g. exit statement.
Return_Type Function_Name (Parameter_1, Parameter_2, ...) {
  Statements
  return Return_Value; // return statement
}

Predefined Functions

C++ comes with in-built libraries of functions for us to use. In order to use them, we must #include the appropriate libarary. Here are some math functions:
  • <cstdlib>
    • abs(int i) - returns absolute value of int
    • labs(long i) - returns absolute value of long int
    • rand() - returns a pseudo-random number in the range between 0 and RAND_MAX. More details in the Random Number Generator section below.
  • <cmath>
    • fabs(float i) - returns absolute value of float
    • pow(double x, double y) - returns x^y

Random Number Generator

To generate a random number, we can use the rand() function from the <cstdlib> library. rand() does not take any arguments and returns a value between 0 and RAND_MAX (a library-dependent constant defined in <cstdlib>). To use this to our intended implementation, we perform:
  1. Scaling - rand() % Scale returns a value in the range 0 to Scale - 1
  2. Shifting - rand() % Scale + Shift returns a value in the range Shift to Scale + Shift - 1

Random Number Seed

Seeds can be used to alter the sequence of pseudo-random numbers. We use the function srand(int seed_value) for this.
  • srand(1) performs the same action as rand().
  • In order to produce random-like numbers, we use runtime values such as time() provided in the <ctime> library. e.g.
    #include <cstdlib.h>
    #include <time.h>
    srand (time(NULL));
    printf ("Random number: %d\n", rand() % 100 + 1); // random number in range 1 to 100
    

Programmer-Defined Functions

There are 3 components to using functions:
  1. Function Declaration - Provides information to compiler to properly interpret function calls.
  2. Function Definition - Provides implmentation of function.
  3. Function Call - Transfers control flow to function.

Function Declaration

Also known as the function prototype. It is placed before any calls, most commonly in header files. It provides the compiler with information on how to properly interpret calls. The compiler only needs to know:
  • Return type
  • Function name
  • Parameter list - formal parameter names not needed but should be left in for readability
Syntax: Return_Type Function_Name (Parameter_List);

Function Definition

This is the implementation of the function. Statements are placed within the enclosing braces. The return statement sends data back to caller. At this point (or end of block for void functions) transfers the control flow back to the point of function call.
Syntax:
Return_Type Function_Name (Parameter_List) {
  Statements
  return Return_Type;
}

Function Call

Calls the specified function.
Syntax: Variable = Function_Name(Argument_List);

Scope

Functions should be self-contained and not interfere with other functions. There are times when a function might have a same variable name as one outside of the function. The scope of the variables determine which one is being used. There are 2 types of variable scopes:
  1. Local scope
  2. Global scope
  3. Block scope

Local Scope

Local variables are declared inside the body of a function and are available only within that function's scope. This allows for variables with the same names being declared in different functions. Advantages of local variables:
  • maintains control over data
  • used on a need-to-know basis
  • functions should declare what local data is needed

Global Scope

Global variables are declared outside of the function body and is accessible to all functions in that file. This is commonly used for constants. Global variables are seldom used as it lacks control over their usage.

Block Scope

Variables are declared inside a compound (block) statement. All function definitions are blocks, which provides local "function-scope". e.g. loop blocks
Block scopes can also be nested. Variables have their scope limited to the innermost enclosing braces.

Parameters

Parameter vs Argument

  • Formal Parameters
    • listed in function declaration
    • used in body of function
    • placeholder that is filled in with something when the function is called
  • Argument
    • used to fill in a formal parameter
    • listed in parenthesis in function call
    • plugged in to formal parameters when function is invoked
There are 2 methods of passing arguments as parameters:
  1. Call-by-value: copy of value is passed
    • local variable
    • only value of arg is passed in
    • if modified, only local copy is mutated
  2. Call-by-reference: address of actual argument is passed
    • provides access to actual argument
    • data provided by argument can be modified by the function
    • Reference is specified by the ampersand & after the data type: datatype& var (see more here.)
    • Reduces overhead because no need to copy over entire object, only address
    • If argument should not be modified, we add the const keyword. e.g. void fun (const int i){}
void getNumbers(int num1, int& num2) {
  num1 += num2;  // local copy of num1 is changed
  num2 += num1;  // original copy of num2 is changed
}

Default Arguments

Allows function call to omit some arguments. Specify default value in parameter list of function declaration. e.g.
void showVolume(int length, int width = 1, int height = 1);
showVolume(1, 2, 3);  // all arguments provided
showVolume(1, 2); // length, width provided. height defaulted to 1
showVolume(7); // length provided. width, height defaulted to 1

Const Modifier

To protect a variable from being changed, the const keyword is placed before the type.

Constant Parameters

This can be used to make parameters constant, especially when passing in references that should not be changed. For example, it is desirable to pass in classes as references (less overhead), but we might not want the object to be changed. e.g. bool isLarger(const int& a, const int& b);

Constant Functions

If a member function should not change the value of calling object, we can mark the function with a const modifier by appending it at the end of the function declaration. As a rule of thumb, if a function does not need to modify its containing class, it should be declared as a constant function. e.g. void output() const;
* Constant objects can only call constant functions.

Mutable Modifier

mutable fields can be changed within constant functions. For example,
struct Student {
  mutable int numMethodCalls = 0;
  float grade() const {
    ++numMethodCalls;
    return
  }
}

Inline Functions

Inline functions are a way to reduce overhead for smaller functions by telling the compiler to replace all calls to this function with its definition (similar to macros).

Inline Non-Member Functions

To make non-member (not part of a class) function inline, we declare it like a normal function, but prepend inline to the function's definition and place it in a header file (to avoid an "unresolved external" error from the linker when trying to call it from other .cpp files).

Inline Member Functions

To make a member function inline, we place the function definition within the class definition, which makes it automatically inline.

Overloading

C++ allows 2 or more different definitions to the same function name, but with different parameter lists (i.e. different number of parameters or parameters of different types). The compiler decides which function to call based on the signature of the function call.

Overloading Resolution

  1. First looks for exact signature - no argument conversion required
  2. Then looks for compatible signatures - where automatic type conversion is possible in the following order:
    1. Promotion (e.g. int to double) - no loss of data
    2. Demotion (e.g. double to int) - possible loss of data

Overloading Built-In Operators

We can overload built-in functions to work with programmer-defined types. To overload an operator, we add the operator keyword after the data type and before the operator. For example, the following overloading of the + operator allows us to add two Money objects together:
const Money operator +(const Money& amount1, const Money& amount2) {
 int allCents1 = amount1.getCents() + amount1.getDollars()*100;
 int allCents2 = amount2.getCents() + amount2.getDollars()*100;
 int sumAllCents = allCents1 + allCents2;
 int absAllCents = abs(sumAllCents); // Money can be negative
 int finalDollars = absAllCents / 100;
 int finalCents = absAllCents % 100;

 if (sumAllCents < 0) {
  finalDollars = -finalDollars;
  finalCents = -finalCents;
 }

 return Money(finalDollars, finalCents);
}

Returning By const Value

Note that the above example for the + operator returns a constant object. If it didn't, we could modify the returned anonymous object which would make no sense.
Money operator +(const Money& amount1, const Money& amount2);
Money sum = (m1 + m2).getDollars();  // possible since not const

Overloading Operators as Member Functions

We can also overload operators as member functions, as part of a class. Allows access to private member variables without having to go through the accessor/mutator methods. This requires the overloaded function to take in only one parameter, since the calling object serves as the first parameter.
const Money::Money operator +(const Money& amount2);

Overloading >> And << Operators

Overloading the inserter and extractor operators can be used to input and output objects of programmer-defined classes. For example, let's look at the inserter operator:
ostream& operator <<(ostream& outputStream, const Money& amount) {
  int absDollars = abs(amount.dollars);
  int absCents = abs(amount.cents);
  if (amount.dollars < 0 || amount.cents < 0)
    //accounts for dollars == 0 or cents == 0
    outputStream << "$-";
  else
    outputStream << '$';
  outputStream << absDollars;
  if (absCents >= 10)
    outputStream << '.' << absCents;
  else
    outputStream << '.' << '0' << absCents;
  return outputStream;
}
Note that the overloaded operator returns a reference. This allows us to chain inserter operators. The extractor operator is overloaded in a similar way, but the second argument receives the input value, so it cannot be a constant.
istream& operator >>(istream& inputStream, Money& amount) {
 char dollarSign;
 inputStream >> dollarSign;
 if (dollarSign != '$') {
   cout << "No dollar sign in Money input.\n";
   exit(1);
 }
 double amountAsDouble;
 inputStream >> amountAsDouble;
 amount.dollars = amount.dollarsPart(amountAsDouble);
 amount.cents = amount.centsPart(amountAsDouble);
 return inputStream;
}

Overloading = Operator

The assignment operator is overloaded as a member operator. The default assignment operator copies values of the member vars from one object to the corresponding member vars of the other object. The operator should return a reference to allow chaining (use this pointer).
StringClass& StringClass::operator=(const StringClass& rtSide) {
  if (this == &rtSide) // if right side same as left side
    return *this;
  else{
    capacity = rtSide.length;
    length = rtSide.length;
    delete [] a;
    a = new char[capacity];
    for (int i = 0; i < length; i++)
      a[i] = rtSide.a[i];
    return *this;
  }
}


Overloading ++ And -- Operators

As both the increment and decrement operators have prefix and postfix versions, we need to distinguish them.
  • Prefix Version: ++x
    • overload as nonmember operator with one parameter
    • overload as member operator with no parameter
  • Postfix Version: x++
    • add parameter of type int. Note that this is just a marker for compiler. Do not give a second int arg when invoking it
class IntPair {
public:
  IntPair operator++(); // Prefix version
  IntPair operator++(int); // Postfix version
}
IntPair IntPair:: operator++() { // Prefix version
 first++;
 second++;
 return IntPair(first, second);
}
IntPair IntPair:: operator++(int ignoreMe) { // Postfix version
 int temp1 = first;
 int temp2 = second;
 first++;
 second++;
 return IntPair(temp1, temp2);
}

Overloading Array Operator []

The array operator can be overloaded (as a member function) so it can be used with objects of the class. To use it on the left-hand side of an assignment, the operator must return a reference.
char& CharPair:: operator[](int index) {
 if (index == 1) return first;
 else if (index == 2) return second;
 else {
   cout << "Illegal index value.\n";
   exit(1);
 }
}

Rules on Overloading Operators

  1. When overloading an operator, at least one parameter of the resulting overloaded operator must be of a class type
  2. Most operators can be overloaded as a member of the class, a friend of the class, or a nonmember, nonfriend
    • Overloading l-value: int& f();
    • Overloading r-value: const int& f() const;
  3. The following operators can only be overloaded as (non-static) members of the class: =, [], ->, and ()
  4. Cannot create a new operator, can only overload existing operators
  5. Cannot change the number of arguments that an operator takes (excluding difference between member and non-member overloaded operator parameters)
  6. Cannot change precedence of an operator. An overloaded operator has the same precedence as the ordinary version of the operator. e.g. x*y + z => (x*y) + z
  7. The following operators cannot be overloaded: ., ::, sizeof, ?:, .*
  8. Overloaded operators cannot have default arguments

Automatic Type Conversion

Following the previous example of overloading as a member function, consider the following case:
Money baseAmount(100, 60), fullAmount;
fullAmount = baseAmount + 25;
fullAmount.output();
Since the + was not overloaded for ints, the compiler looks for a constructor that takes a single argument of int and converts it to an object of type Money. Once converted, the program applies the overloaded operator function onto the it.
Note that 25 + baseAmount would not work as 25 cannot be a calling object. This can be handled if we overload the + operator as a non-member.
Advantages of overloading as a non-member:
  1. allows us to combine variables of different types
  2. gives automatic type conversion of all arguments
On the other hand, overloading as a member gives us the efficiency of bypassing accessor/mutator methods and diirectly accessing member variables. Overloading as a friend function offers both advantages.

Friend Functions

A friend function of a class is not a member function of the class but it has access to private members/functions of that class. This makes friend functions more efficient as it reduces overhead. They are essentially nonmember functions that have all the privileges of member functions.
We declare friend functions of a class in the class definition, similar to member functions, but they are not member functions. They are ordinary non-member functions with extra access to data members of the class. They are called the same way as a non-member function (no need for dot operator).
class Class_Name {
public:
  friend Friend_Function();
};

Assert Macro

An assertion is a true or false statement used in debugging and testing programs. It is used to check correctness of provided arguments. It does not have a return value and terminates the program if the assert condition is false so that the error can be investigated. Defined in the <cassert> library.
Syntax: assert (Assert_Condition);
To turn assertions off, add #define NDEBUG before the include statements. To turn it back on, remove or comment out that line. e.g.
#define NDEBUG
#include <cassert>
void getAge(int age){
  assert (age >= 0); // program terminates if age is negative
}