Functions
Functions are the building blocks of programs. The 3 advantages of functions are:
- Divide & Conquer
- Readability
- Re-usability
There are 2 types of functions:
-
Returns a value - function returns a value (can be used as a value). Must include a
return
statement
-
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:
- Scaling -
rand() % Scale
returns a value in the range 0 to Scale - 1
- 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.
Programmer-Defined Functions
There are 3 components to using functions:
- Function Declaration - Provides information to compiler to properly interpret function calls.
- Function Definition - Provides implmentation of function.
- 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:
- Local scope
- Global scope
- 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:
-
Call-by-value: copy of value is passed
- local variable
- only value of arg is passed in
- if modified, only local copy is mutated
-
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
- First looks for exact signature - no argument conversion required
- Then looks for compatible signatures - where automatic type conversion is possible in the following order:
-
Promotion (e.g.
int
to double
) - no loss of data
-
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
-
When overloading an operator, at least one parameter of the resulting overloaded operator must be of a class type
-
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;
-
The following operators can only be overloaded as (non-static) members of the class: =, [], ->, and ()
-
Cannot create a new operator, can only overload existing operators
-
Cannot change the number of arguments that an operator takes (excluding difference between member and non-member overloaded operator parameters)
-
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
-
The following operators cannot be overloaded:
.
, ::
, sizeof
, ?:
, .*
-
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:
-
allows us to combine variables of different types
-
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
}