Professional Documents
Culture Documents
1009: Introduction to Programming
Lab 2: Testing Programs, More Loops, Arrays, Vectors,
Strings and Files
Most of the material for this lab has been sourced from http://www.learncpp.com
1. Testing Your Program
Software testing is any activity aimed at evaluating an attribute or capability of program or system (and
its components) to determine whether or not it meets its required results. Software testing is
performed with the intent of finding software bugs; we execute a system in order to identify gaps,
errors or missing requirements. ANSI/IEEE 1059 standards define testing as “a process of analyzing a
software item to detect differences between existing and required conditions (i.e. defects, errors, bugs)
and to evaluate the features of the software item”. Testing can also be stated as the process of
validating and verifying that a software program, application or product meets the business and
technical requirements for which it was designed and developed, and that it works as expected.
Why perform software testing? As humans, we make mistakes all the time. Some of these mistakes are
unimportant, but some may be expensive and dangerous, depending on the application. We need to
check everything and anything we produce because things can always go wrong. As mentioned
previously, software testing is really required to point out the defects and errors that were made during
the development phases. It is necessary to ensure that the application should not result in any failures
because they can be very expensive in the future or in the later stages of development.
Testing is crucial to software quality; it is important to ensure quality of a product (quality assurance
(QA)). It is also necessary for the verification and validation or reliability estimation. It is essential for the
customer’s satisfaction and required for ensuring effective performance of software applications or
products. Software testing is a trade‐off between budget, time and quality.
1.1 Types of Testing and Testing Methods
During the software development life cycle, we may wish to perform different types of testing on our
product. At the highest level, testing can be classified into manual and automation testing.
1 | P a g e
Manual testing, as the name implies, refers to testing software manually, i.e. without using an
automated tool or script. This is the type of testing with which we are familiar and which we will focus
on. The tester takes over the role of an end‐user and tests the software to identify any unexpected
behavior of bugs. Testers use test plans, test cases or test scenarios to test a software to ensure the
completeness of testing. The different stages for manual testing include: unit testing, integration
testing, system testing and user acceptance testing.
Automation testing, also known as Test Automation, is when the tester writes scripts and uses another
software to test the product. This process involves the automation of a manual process.
Automation testing is used to re‐run the test scenarios that were performed during manual testing,
quickly and repeatedly. We shall see this in the form of component drivers. Test automation is typically
used in large, critical projects, in projects that require testing the same areas frequently, in projects
where the requirements do not change frequently and when availability of time is a critical factor. There
are a number of commercial software testing tools that can be used for automation testing.
Testing can also be classified as static or dynamic. Static testing involves reviews, code walkthroughs and
inspections. Static testing finds defects without executing code and starts early in the life cycle and
performed during the verification stage. It is performed without the use of a computer, i.e. we do not
use a compiler nor do we build the program. This may involve identifying syntax errors and data flow
errors.
Dynamic testing however involves the execution of the code to demonstrate the result of running tests.
This is performed during the validation process and requires the computer for testing. Examples involve
unit testing, integration testing and system testing. Dynamic testing may begin before the program is
100% complete in order to test particular sections of code. Typical techniques include the use of stubs
and drivers or execution from a debugger environment.
1.2 Component Testing
The use of component drivers falls under the heading of component testing. Component testing is a
method where testing of each component in an application is done separately. It is also known as
module testing and may be done in isolation from the rest of the system. In such cases, stubs and
drivers are used to replace the missing software and simulate the interface between the software
components in a simple manner.
2 | P a g e
Consider the following example: suppose there is an application consisting of two modules, say function
A and function B. The developer has developed function A and wants to test it. But in order to test
function A completely, a few of its functionalities are dependent on function B, specifically function A is
supposed to call function B. But function B has not yet been developed. In such a case, to test function A
completely, function B can be replaced by a stub. A stub is called from the software component to be
tested.
Say now the developer had developed function B and not function A, but he wants to test function B.
Since function B can only be called from function A (which has not yet been created), in order to run
function B, function A can be replaced by a driver. A driver calls the component to be tested. These two
cases are illustrated in the following diagram:
1.2.1 The Box Approach
The box approach of software testing are the two most widespread testing methods used in the
software engineering community. Testing methods are traditionally divided into white and black
box testing and differ in the point of view that a test engineer takes when designing test cases.
White Box Testing:
Also known as clear box testing, open box testing or glass box testing
A software testing in which the internal structure/design/implementation is known to the tester
Examines program structure, as opposed to functionality, and derives test data from program
logic/code. Goes beyond the user interface.
Tester chooses inputs to exercise paths through the code and determines appropriate outputs
Techniques include: statement coverage, branch coverage and path coverage
3 | P a g e
Can be applied to unit, integration and system testing levels. Typically most used for unit testing.
Advantages:
Because tester has knowledge of source code, it is easy to find out which type of data can help
in testing application effectively
Helps in optimizing code
Extra lines of code can be removed which can bring hidden defects (dead code) – better
programming practice
Disadvantages:
Expensive – both time (it is an exhaustive process) and money (skilled testers are needed) are
spent to perform white box testing
Impossible to look through code to find EVERY hidden error – many paths will go untested
Difficult to maintain white box testing as it requires specialized tools like code analysers and
debuggers
Black Box Testing
Also known as specifications based testing
Unlike white box testing, this is a technique of testing without having any knowledge of the
interior workings of the application.
Tester is oblivious to the system architecture and does not have access to source code.
Tester interacts with the system’s user interface by providing inputs and examining outputs
without knowing how and where the inputs are worked upon
Tester examines functionality of an application based on specifications
Applied at unit, integration, system and acceptance testing
Techniques include: equivalence class, boundary value analysis, domain tests, etc.
Advantages:
Well suited and efficient for large code segments
Code access not required
Separates user’s perspective from developer’s perspective through visibly defined roles
4 | P a g e
Disadvantages:
Limited coverage, since only a selected number of test scenarios are performed
Inefficient testing since tester only has limited knowledge about application
Blind coverage
Test cases difficult to design
1.3 Errors
Now that we have been exposed to an overview of testing, let us now look into the different types or
errors that testing determines. As we said earlier, humans make mistakes all the time – no matter how
careful you are, errors are your constant companion. With practice, you can get better at not making
errors, and even better at finding and correcting them. In programming, there are three kinds of errors:
syntax errors, logic errors and runtime errors.
Syntax errors – errors in spelling and “grammar”. Spelling errors in code occur when keywords
are spelt incorrectly and when variables previously declared are misspelt later on. Grammatical
errors in code refer to such things as omitting a semicolon at the end of a line, using a variable
that was not declared and incorrectly nested braces. Syntax errors are the easiest to find and
correct and may be done via code walkthroughs or using a compiler. The compiler tells you
where it got into trouble and the program will not execute until these errors are corrected.
Logic errors – these occur when your program compiles and runs but does the wrong thing. The
compiler fails to detect logic errors – you do not get error messages and the program need not
necessarily crash. The only clue to the existence of logic errors is the production of wrong
solutions. These can be rectified by using a debugger to step through your program and watch
what it does.
Runtime errors – if there are no syntax errors, errors may be encountered while the program is
running. These errors are denoted by error messages during runtime and program crashes, e.g.
stack overflows or incorrectly indexing an array. Runtime errors are intermediate in difficulty
since you are told where the program goes wrong, but you need to trace back from there to
figure out where the problem originated.
5 | P a g e
1.4 White Box Testing
Typically, white box testing is done within the code implementation i.e. writing a driver or stub function
to conduct the testing procedure. The purpose of white box testing is to test individual functions within
the code by calling them inside the driver or stub function with different assorted input parameters. The
choice of these input parameters is determined by the necessary test cases to be conducted.
Consider the code snippet below where the area of the rectangle can be calculated using the area
function. If white box testing was to be conducted instead using multiple different inputs for the width
and height, a driver function could be written to conduct the area function for each one of them. The
driver function would then be called within the main function. The code snippet for the driver function
can be seen below:
void Driver(){
std::cout << area(1,2) << std::endl;
std::cout << area(2,3) << std::endl;
std::cout << area(3,4) << std::endl;
}
int main(){
Driver();
return 0;
}
The example shows white box testing for one function using different inputs. Consider the scenario if
multiple shapes required their area to be calculated. White box testing would allow for testing the area
of each shape, as well as any other functions written for each shape. Furthermore, white box testing is
6 | P a g e
not limited to just one function. Multiple different driver functions could be written for white box
testing.
1.5 Black Box Testing
Black box testing is done when outside of the code’s implementation since the tester does not have
access to any of the source code. In such an event, the only thing that the tester can do is write a batch
script native to the operating system’s command prompt to automate testing of the overall program
itself.
However, using a batch script automation process for black box testing requires an assumption to be
validated. The assumption is such that the program is written to accept input parameters from the
command line. If this assumption is violated i.e. it is not true, black box testing must be done manually.
In C++, it is possible to accept command line arguments but this means the main() function must contain
a special input argument list which can look like either one of the following forms. Please note that both
forms are equivalent and are entirely dependent on the programmer’s choice:
The first argument argc is the number of elements within the array which is the second argument argv.
The second argument is always a pointer to a character array because any arguments passed from the
command line are considered to be character arrays and can only be passed using pointers. Spaces
between clusters of characters on the command line indicates that each character cluster is treated as a
separate array argument. Consider the following code snippet below:
7 | P a g e
It can be noticed that argv[0] is the path and name of the compiled program itself. Any other command
line arguments would start from argv[1] and so forth with the numerical index increasing for the
number of arguments.
As stated before, all input command line parameters are considered as character arrays. If they must be
treated as some other data type such as an integer, the data type must be converted. The cstdlib must
be included to facilitate these conversions and the functions used to transform these characters would
be atoi(), atoll() and atof() which converts ASCII character ararys to data types int, long and double
respectively. There are other conversion functions within the cstdlib that can be utilized.
Consider the code snippet below that shows an example of how the atoi() function works:
#include <iostream>
#include <cstdlib>
using namespace std;
int main(int argc, char* argv[]) {
for(int i = 1; i < argc; i++)
cout << atoi(argv[i]) << endl;
return 0;
}
Attempt to use the program by passing any number of arguments on the command line, be it integers,
decimals or random characters. Observe the effect as to how the compiled program treats each case.
Since the program is written using the special input parameters for the main() function, it is possible to
write a batch script in the Windows operating system to automate any testing procedures. For instance,
if the program is named test_program and the code snippet above is under test, consider the batch
script below that shows an example of how a batch script can be written using Notepad:
test_program 1 2 3
test_program 1.23 4.56 7.8901
test_program 1 1.23 a
Please note that when saving this file in Notepad, it must be saved with the .bat extension and that the
location of the batch file must be in the same directory of the compiled program. Run the batch file by
calling it in the command prompt and observe the effects as to how the compiled program operates.
8 | P a g e
To summarize what is learnt in this section, using this programming technique would allow for faster
automation black box testing than using a manual method. It is also possible to write the results of the
black box testing procedure by modifying the source code to write the results to a file instead of using
the cout command to print to the command prompt. An attempt of this should be made outside of the
lab time.
1.6 Handling Errors: Exception Handling
An exception is a problem that arises during the execution of a program (i.e. during runtime). A C++
exception is a response to an exceptional circumstance (like runtime errors) that arises while a program
is running, such as an attempt to divide by zero. Why, then, do we use exceptions as opposed to, say, if‐
statements? Using exceptions for error handling makes code simpler, cleaner and less likely to miss
errors. With if‐statements, your error handling and your normal code are closely intertwined. Your code
gets messy and it becomes hard to ensure that you have dealt with all errors. Exceptions provide a way
to transfer control from one part of a program to special functions called handlers. C++ exception
handling is built upon three keywords: try, catch and throw.
throw – a program throws an exception when a problem shows up. This is done using the throw
keyword
catch – A program catches an exception with an exception handler at the place in the program
where you want to handle the problem. The catch keyword indicates the catching of an
exception. The catch block is the exception handler itself.
try – a try block identifies a block of code for which particular exceptions will be activated. The
try block is used to place a portion of code under “exception inspection”. It is followed by one or
more catch blocks.
Assuming a block will raise an exception, a method catches an exception using a combination of try and
catch keywords. A try/catch block is placed around the code that might generate an exception. Code
within a try/catch block is referred to as protected code. The following snippet shows the syntax for
using try/catch:
try
{
// protected code
9 | P a g e
} catch( ExceptionName e1 )
{
// catch block
} catch( ExceptionName e2 )
{
// catch block
} catch( ExceptionName eN )
{
// catch block
}
You can list multiple catch statements to catch different types of exceptions in case your try block raises
more than one exception in different situations. The catch block following the try block catches any
exception. You can specify what type of exception you want to catch and this is determined by the
exception declaration that appears in brackets following the keyword catch. In the code above, we catch
exceptions e1, e2 and eN of type ExceptionName. If you want to specify a catch block that should handle
any type of exception that is thrown in a try block, you must put ellipsis (…) between the parentheses.
For example:
try
{
// protected code
} catch(...)
{
// code to handle any exception
}
Exceptions can be thrown anywhere within a block of code using throw statements. The operand of the
throw statements determines a type for the exception and can be any expression and the type of the
result of the expression determines the type of exception thrown. Consider a divide by zero condition:
10 | P a g e
double division(int a, int b)
{
if( b == 0 )
{
throw "Division by zero condition!";
}
return (a/b);
}
Since these three statements for exception handling are used together, let us look at an example which
throws a division by zero exception and catches it in a catch block.
#include <iostream>
using namespace std;
11 | P a g e
z = division(x, y);
cout << z << endl;
} catch (const char* msg) {
cerr << msg << endl;
}
return 0;
}
So the typical order of exception handling is: when the exception arises in the try block, an exception is
thrown that transfers control to the exception handler, which is the catch block. If no exception is
thrown, the code continues normally and all handlers are ignored. The exception can also be thrown
from inside the try block, as shown below:
int main () {
try
{
throw 20;
}
catch (int e)
{
cout << "An exception occurred. Exception Nr. " << e << '\n';
}
return 0;
}
12 | P a g e
2. More Loops
The previous lab introduced the topic of loops, with the for loop being a highlight. We will now
investigate while and do‐while loops in particular.
2.1 While Loops
The while statement is the simplest of the loops that C++ provides, and it has a definition very similar to
that of an if statement. The definition for the while statement can be seen below:
while (expression)
statement;
A while statement is declared using the while keyword. When a while statement is executed, the
expression is evaluated. If the expression evaluates to true (non‐zero), the statement executes.
However, unlike an if statement, once the statement has finished executing, control returns to the top
of the while statement and the process is repeated. Let’s take a look at a simple while loop in action
within the code below:
#include <iostream>
int main()
{
int count = 0;
while (count < 10)
{
std::cout << count << " ";
++count;
}
std::cout << "done!";
return 0;
}
First, count is initialized to 0. We can see that “0 < 10” evaluates to true, so the statement block
executes. The first statement prints 0, and the second increments the count to 1. Control then returns
back to the top of the while statement. We now see that “1 < 10” evaluates to true, so the code block is
executed again. The code block will repeatedly execute until count is 10, at which point “10 < 10”
evaluates to false, and the loop will exit.
It is possible that a while statement can execute zero times. Consider the code below:
#include <iostream>
13 | P a g e
int main()
{
int count = 15;
while (count < 10)
{
std::cout << count << " ";
++count;
}
std::cout << "done!";
return 0;
}
The condition “15 < 10” immediately evaluates to false, so the while statement is skipped. The only
thing that the program prints is “done!”
On the other hand, if the expression always evaluates to true, the while loop will execute forever. This is
referred to as an infinite loop. The code below shows an example of this:
#include <iostream>
int main()
{
int count = 0;
while (count < 10) // this condition will never be false
std::cout << count << " "; // so this line will repeatedly execute
return 0; // this line will never execute
}
Count is never incremented in this program, so “count < 10” will always be true. The only way to exit an
infinite loop is through a return statement, a break statement, an exit statement or the user killing the
program.
Often, we want a loop to execute a certain number of times. To do this, it is common to use a loop
variable, often called a counter. A loop variable is an integer variable that is declared for the sole
purpose of counting how many times a loop has executed. In the examples above, the variable “count”
is a loop variable.
Loop variables are often given simple names, such as “i”, “j” or “k”. However, naming variables like this
has one major problem. If you want to know where in your program a loop variable is used, and you use
the search function on “i”, “j” or “k”, all the instances of these characters would be returned. A better
idea is to use “iii”, “jjj” or “kkk” as your loop variable names. These names are more unique and it makes
14 | P a g e
searching for your loop variables easier. An even better idea is to use “real” variable names such as
“count”, or a name that gives more detail about what you are counting.
Each time a loop executes, it is called an iteration. Since the loop body is typically a block, and because
that block is entered and exited with each iteration, any variables declared inside the loop are created
and then destroyed with each iteration. The code below shows that the variable ‘x’ is created and
destroyed 5 times:
#include <iostream>
int main()
{
int count = 1;
int sum = 0; // sum is declared up here because we need it later (beyond the loop)
while (count <= 5) // iterate 5 times
{
int x; // x is created here with each iteration
std::cout << "Enter integer #" << count << ':';
std::cin >> x;
sum += x;
// increment the loop counter
++count;
} // x is destroyed here with each iteration
std::cout << "The sum of all numbers entered is: " << sum;
return 0;
}
For fundamental variables, this is fine. For non‐fundamental variables, which you will meet later on, this
causes performance issues. You may want to consider defining non‐fundamental variables before the
loop. This is another one of the cases where you might declare a variable well before its first actual use.
Note that the variable “count” is declared outside the loop. This is necessary because we need the value
to persist across all iterations, not to be destroyed with each iteration.
It is also possible to nest loops inside of other loops. In the following example, the inner and outer loops
each have their own counters. However, note that the loop expression for the inner loop makes use of
the counter of the outer loop as well.
#include <iostream>
15 | P a g e
// Loop between 1 and 5
int main()
{
int outer = 1;
while (outer <= 5)
{
// loop between 1 and outer
int inner = 1;
while (inner <= outer)
std::cout << inner++ << " ";
// print a newline at the end of each row
std::cout << "\n";
++outer;
}
return 0;
}
2.2 Do‐While Loops
One interesting thing about the while loop is that if the loop condition is false, the while loop will not
execute at all. It is sometimes the case that we know we want a loop to execute at least once, such as
when displaying a menu. To facilitate this, C++ offers the do‐while loop. Its definition can be seen below:
do
statement;
while (condition);
The statement in a do‐while loop always executes at least once. After the statement has been executed,
the do‐while loop checks the condition. If the condition is true, the path of execution jumps back to the
top of the do‐while loop and executes it again. The code below shows an example of using a do‐while
loop to display a menu to the user and wait for the user to make a valid choice:
#include <iostream>
int main()
{
// selection must be declared outside do/while loop
int selection;
do
{
std::cout << "Please make a selection: \n";
std::cout << "1) Addition\n";
std::cout << "2) Subtraction\n";
16 | P a g e
std::cout << "3) Multiplication\n";
std::cout << "4) Division\n";
std::cin >> selection;
}
while (selection != 1 && selection != 2 &&
selection != 3 && selection != 4);
// do something with selection here
// such as a switch statement
std::cout << "You selected option #" << selection << "\n";
return 0;
}
One interesting thing about the above example is that the selection variable must be declared outside of
the do block. If the selection variable was to be declared inside the do block, it would be destroyed
when the do block terminates, which happens before the while conditional is executed. We need the
variable to be used in the while conditional. Consequently, the selection variable must be declared
outside the do block.
Now, given the code below, investigate what is wrong with the loop:
#include <iostream>
int main(){
int counter = 0;
while {counter > 100}
if (counter % 2 == 1)
std::cout << counter << " is odd." << std::endl;
else
std::cout << counter << " is odd." << std::endl;
++counter;
int counter = 0;
while {counter > 100}
if (counter % 2 == 1)
std::cout << counter << " is odd." << std::endl;
else
std::cout << counter << " is odd." << std::endl;
++counter; // same as: counter = counter + 1;
return 0;
}
2.3 Challenge Questions
Write code to do the following:
1) Repeatedly print the value of variable “xValue”, decreasing it by 0.5 each time, as long as
“xValue” remains positive. The user should specify the initial value of “xValue”.
17 | P a g e
2) Print the square root of the first 25 odd numbers.
3) Calculate “n!”, where “n” is supplied by the user
3. Arrays
An array is an aggregate data type that lets us access many variables of the same type though a single
identifier.
Consider the case where you want to record the test scores for 30 students in a class. Without arrays,
you would have to allocate 30 almost identical variables. Arrays give us an easier way to do this. The
following array definition is essentially equivalent:
int testScore[30]; // allocate 30 integer variables in a fixed array
In an array variable declaration, we use square brackets, ‘[‘ and ‘]’, to tell the compiler both that this is
an array variable (instead of a normal variable), as well as how many variables to allocate (referred to as
the array length).
The above line of code declares a fixed array named “testScore”, with a length of 30. A fixed array, also
called a fixed length array or fixed array size, is an array where the length is known as compile time.
When “testScore” is initialized, the compiler will allocate 30 integers.
Each of the variables in an array is called an element. Elements do not have their own unique names.
Instead, to access individual elements of an array, we use the array name, along with the subscript
operator [], and a parameter called the subscript or index, that tells the compiler which element we
want. This process is referred to as subscripting or indexing the array.
The first element in our array is testScore[0]. The second is testScore[1]. The tenth is testScore[9]. The
last element in our testScore array is testScore[29]. This is great because we no longer need to keep
track of a bunch of different (but related) names; we can just vary the subscript to access different
elements.
It should be noted at this point that C++ arrays always count starting from zero. Hence why the first
element is at position 0 and the last element is at position 29. For an array of length N, the array
elements are numbered 0 through N‐1. This is referred to as the range of an array.
The code below illustrates the definition and indexing of an array:
18 | P a g e
#include <iostream>
int main()
{
int prime[5]; // hold the first 5 prime numbers
prime[0] = 2; // The first element has index 0
prime[1] = 3;
prime[2] = 5;
prime[3] = 7;
prime[4] = 11; // The last element has index 4 (array length‐1)
std::cout << "The lowest prime number is: " << prime[0] << "\n";
std::cout << "The sum of the first 5 primes is: " << prime[0] + prime[1] + prime[2] +
prime[3] + prime[4] << "\n";
return 0;
}
Arrays can be made from any data type. The following code example shows how we declare an array of
doubles:
#include <iostream>
int main()
{
double array[3]; // allocate 3 doubles
array[0] = 2.0;
array[1] = 3.0;
array[2] = 4.3;
cout << "The average is " << (array[0] + array[1] + array[2]) / 3 << "\n";
return 0;
}
In C++, arrays subscripts must always be an integral type (char, short, int, long, bool). These subscripts
can either be a constant or non‐constant value. We have seen so far that the array subscripts used are
integers. This is the most commonly used data type for the array subscript.
When declaring a fixed array, the length of the array (between the square brackets) must be a compile‐
time constant. This is because the length of a fixed array must be known at compile time. This
introduces two limitations:
Fixed arrays cannot have a length based on either user input or some other value calculated at
runtime.
Fixed arrays have a fixed length that cannot be changed.
19 | P a g e
3.1 Initializing an Array
Array elements are treated just like normal variables, and as of such, they are not initialized when
created. One way to initialize an array is to do it element by element as we have seen in the previous
code examples. However, this is a pain, especially as the array gets larger.
Fortunately, C++ provides a more convenient way to initialize entire arrays via use of an initializer list.
The following example is equivalent to the initialization of the “prime” array:
int prime[5] = { 2, 3, 5, 7, 11 }; // use initializer list to initialize the fixed array
If there are more initializers in the list than the array can hold, the compiler will generate an error.
However, if there are less initializers in the list than the array can hold, the remaining elements are
initialized to zero. The following code example illustrates this:
#include <iostream>
int main()
{
int array[5] = { 7, 4, 5 }; // only initialize first 3 elements
std::cout << array[0] << '\n';
std::cout << array[1] << '\n';
std::cout << array[2] << '\n';
std::cout << array[3] << '\n';
std::cout << array[4] << '\n';
return 0;
}
Consequently, to initialize all the elements of an array to zero, you can do this:
// Initialize all elements to 0
int array[5] = { };
If you are initializing a fixed array of elements using an initializer list, the compiler can figure out the
length of the array for you, and you can omit explicitly declaring the length of the array. The following
two lines of code are equivalent:
int array[5] = { 0, 1, 2, 3, 4 }; // explicitly define length of the array
int array[] = { 0, 1, 2, 3, 4 }; // let initializer list set length of the array
20 | P a g e
This not only saves typing, it also means you do not have to update the array length if you add or
remove elements later.
3.2 Arrays and Functions
Passing an array to a function at first glance looks just like passing a normal variable. However, C++
treats arrays differently.
When a normal variable is passed by value, C++ copies the value of the argument into the function
parameter. Since the parameter is a copy, changing the value of the parameter does not change the
value of the original argument.
However, because copying large arrays can be very expensive, C++ does not copy an array when it is
passed into a function. Instead, the actual array is passed. This has the side effect of allowing functions
to directly change the value of array elements. The following code illustrates this point:
#include <iostream>
void passValue(int value) // value is a copy of the argument
{
value = 99; // so changing it here won't change the value of the argument
}
void passArray(int prime[5]) // prime is the actual array
{
prime[0] = 11; // so changing it here will change the original argument!
prime[1] = 7;
prime[2] = 5;
prime[3] = 3;
prime[4] = 2;
}
int main()
{
int value = 1;
std::cout << "before passValue: " << value << "\n";
passValue(value);
std::cout << "after passValue: " << value << "\n";
int prime[5] = { 2, 3, 5, 7, 11 };
std::cout << "before passArray: " << prime[0] << " " << prime[1] << " " << prime[2]
<< " " << prime[3] << " " << prime[4] << "\n";
passArray(prime);
std::cout << "after passArray: " << prime[0] << " " << prime[1] << " " << prime[2] <<
" " << prime[3] << " " << prime[4] << "\n";
return 0;
}
21 | P a g e
In the above example, “value” is not changed in main(), because the parameter value in function
passValue() was a copy of variable “value” in function main(), not the actual variable. However, because
the parameter array in function passArray() is the actual array, passArray() is able to directly change the
value of the elements.
If you want to ensure a function does not modify the array elements passed into it, you can make the
array constant using the keyword const. The code below illustrates this:
// even though prime is the actual array, within this function it should be treated as a
constant
void passArray(const int prime[5])
{
// so each of these lines will cause a compile error!
prime[0] = 11;
prime[1] = 7;
prime[2] = 5;
prime[3] = 3;
prime[4] = 2;
}
The sizeof operator can be used on arrays, and it will return the total size of the array (array length
multiplied by element size). This will not work properly for arrays that have been passed into functions.
The code below gives an example of this:
void printSize(int array[])
{
std::cout << sizeof(array) << '\n'; // prints the size of a pointer, not the size of
the array!
}
int main()
{
int array[] = { 1, 1, 2, 3, 5, 8, 13, 21 };
std::cout << sizeof(array) << '\n'; // will print the size of the array
printSize(array);
return 0;
}
It is possible to determine the length of a fixed array by dividing the size of the entire array by the size of
an array element. The code below illustrates this:
int main()
{
int array[] = { 1, 1, 2, 3, 5, 8, 13, 21 };
std::cout << "The array has: " << sizeof(array) / sizeof(array[0]) << "elements\n";
22 | P a g e
return 0;
}
Note that the size of the entire array is equal to the length of the array multiplied by the size of an
element.
Recall that an array of length N has array elements 0 through N‐1. What happens if you try to access an
array with a subscript outside of that range? Consider the code below:
int main()
{
int prime[5]; // hold the first 5 prime numbers
prime[5] = 13;
return 0;
}
In this program, the array is of length 5, but an attempt is made to write a test score into the 6th element
(index 5).
C++ does not check to make sure that your indices are valid for the length of your array. In the above
example, the value of 13 will be inserted into memory where the 6th element would have been had it
existed. When this happens, you will get undefined behavior, meaning your program will crash.
3.3 Arrays and Loops
It is possible to use a loop variable as an array index to loop through all of the elements of an array and
perform some type of calculation on them. The loop variable in this case would be the array subscript.
This is common wherever you find arrays, you will most certainly find loops used for them. When a loop
is used to access each array element in turn, this is often called iterating through the array. The code
below gives an example of this:
#include <iostream>
int main()
{
int scores[] = { 84, 92, 76, 81, 56 };
const int numStudents = sizeof(scores) / sizeof(scores[0]);
int maxScore = 0; // keep track of our largest score
for (int student = 0; student < numStudents; ++student)
if (scores[student] > maxScore)
23 | P a g e
maxScore = scores[student];
std::cout << "The best score was " << maxScore << '\n';
return 0;
}
In the code above, we use a non‐loop variable called “maxScore” to keep track of the highest score we
have seen. “maxScore” is initialized to zero to represent that we have not seen any scores yet. We then
iterate through each element of the array, and if we find a score that is higher than any we have seen
before, we set “maxScore” to that value. Thus, “maxScore” always represents the highest score out of all
the elements we have searched so far. By the time we have reached the end of the array, “maxScore”
holds the highest score in the entire array.
Loops are typically used with arrays to do one of three things:
1. Calculate a value (e.g. average, total)
2. Search for a value (e.g. highest, lowest)
3. Reorganize the array (e.g. ascending, descending)
When calculating a value, a variable is typically used to hold an intermediate result that is used to
calculate the final value.
When searching for a value, a variable is used to hold the best candidate value seen so far (or the array
index of the best candidate).
Sorting an array is trickier, as it typically involves nested loops.
One of the trickiest part of using loops with arrays is making sure that the loop iterates the proper
number of times. Off‐by‐one errors are easy to make, and trying to access an element that is larger than
the length of the array can have dire consequences. Consider the code below:
int scores[] = { 84, 92, 76, 81, 56 };
const int numStudents = sizeof(scores) / sizeof(scores[0]);
int maxScore = 0; // keep track of our largest score
for (int student = 0; student <= numStudents; ++student)
if (scores[student] > maxScore)
maxScore = scores[student];
std::cout << "The best score was " << maxScore << '\n';
24 | P a g e
The problem with the code is that the conditional in the for loop is wrong. The array declared has 5
elements, indexed from 0 to 4. However, this array loops from 0 to 5. Consequently, on the last
iteration, the array will try to access “scores[5]” but this is undefined. You may end up with a garbage
value which will mean that the value of “maxScore” would be wrong.
Image what would happen if we inadvertently assigned a value to “scores[5]”. We might overwrite
another variable (or part of it), or perhaps corrupt some other data.
When using loops with arrays, always double‐check your loop conditions to make sure you do not
introduce off‐by‐one errors.
3. Vectors
The previous section showed how static arrays can be implemented. This section introduces the usage
of the vector library, which allows for the creation and manipulation of dynamic arrays.
The std::vector datatype provides dynamic array functionality that handles its own memory
management. This means that arrays can be created with their own lengths at runtime, without having
to explicitly allocate memory on compile time. The std::vector datatype lives in the <vector> header.
Consider the code snippet below:
#include <vector>
// no need to specify length at initialization
std::vector<int> array;
std::vector<int> array2 = { 9, 7, 5, 3, 1 }; // use initializer list to initialize array
std::vector<int> array3{ 9, 7, 5, 3, 1 }; // use uniform initialization to initialize
array (C++11 onward)
Note that both in the uninitialized and initialized cases, there is no need to include the array length at
compile time. The std::vector datatype will dynamically allocate memory for its contents as requested.
Just like any typical array, accessing the elements of the array can be done using the [] operator (which
does no bounds checking). In addition, the at() function can be used (which does bounds checking). An
example is given in the code snippet below:
array[6] = 2; // no bounds checking
array.at(7) = 3; // does bounds checking
25 | P a g e
In either case, if an element is requested that is off the end of the array, the vector will not
automatically resize itself.
When a vector variable goes out of scope, it automatically deallocates the memory it controls, if
necessary. This prevents memory leaks in your program.
Vectors also keep track of their own length using the size() function. An example is given in the code
below:
#include <vector>
#include <iostream>
int main()
{
std::vector<int> array{ 9, 7, 5, 3, 1 };
std::cout << "The length is: " << array.size() << '\n';
return 0;
}
It is impossible to resize a static array, since its size is set at compile time. However, a vector can be
resized at any point during runtime, using the resize() function. Consider the code below:
#include <vector>
#include <iostream>
int main()
{
std::vector<int> array{ 0, 1, 2 };
array.resize(5); // set size to 5
std::cout << "The length is: " << array.size() << '\n';
return 0;
}
The code snippet below illustrates some of the functionality of vectors that can be used:
// C++ program to illustrate the
// capacity function in vector
#include <iostream>
#include <vector>
using namespace std;
int main()
{
vector<int> g1;
for (int i = 1; i <= 5; i++)
g1.push_back(i);
26 | P a g e
cout << "Size : " << g1.size();
cout << "\nCapacity : " << g1.capacity();
cout << "\nMax_Size : " << g1.max_size();
// resizes the vector size to 4
g1.resize(4);
// prints the vector size after resize()
cout << "\nSize : " << g1.size();
// checks if the vector is empty or not
if (g1.empty() == false)
cout << "\nVector is not empty";
else
cout << "\nVector is empty";
// Shrinks the vector
g1.shrink_to_fit();
cout << "\nVector elements are: ";
for (auto it = g1.begin(); it != g1.end(); it++)
cout << *it << " ";
return 0;
}
There are other functions that are useful for vectors, especially modifier functions. A list of these
functions can be found at http://www.cplusplus.com/reference/vector/vector/
4. Strings
A collection of sequential characters is what we refer to as a string. In C++, we use strings to represent
text such as name, addresses, words and sentences. String literals, such as “Hello World!” are placed
between double quotes to identify them as a string.
4.1 C‐Style Strings
Before we learn about C++ strings, we need to know about C‐style strings. A C‐style string is simply an
array of characters that uses a null terminator. A null terminator is a special character (‘\0’, ASCII Code
0) used to indicate the end of the string. More generically, a C‐style string is called a null‐terminated
string.
In order to define a C‐style string, simply declare a char array and initialize it with a string literal:
char myString[] = "string";
27 | P a g e
Although “string” only has 6 letters, C++ automatically adds a null terminator to the end of the string for
us. Consequently, “myString” is actually an array of length 7.
When declaring strings in this manner, since they are character arrays, it is a good idea to use [] and let
the compiler calculate the length of the array. That way if you change the string later, you will not have
to manually adjust the array length.
C‐style strings follow all the same rules as arrays. You can initialize the string upon creation, but you
cannot assign values to it using the assignment operator after that. Since C‐style strings are arrays, you
can use the [] operator to change individual characters in the string. The code below gives an example of
this:
#include <iostream>
int main()
{
char myString[] = "string";
myString[1] = 'p';
std::cout << myString;
return 0;
}
When printing a C‐styled string, cout prints characters until it encounter a null terminator. If you
accidentally overwrite the null terminator in a string, you will not only get all the characters in the string,
but cout will just keep printing everything in adjacent memory slots until it happens to hit a zero.
There are many cases when we do not know in advance how long our string is going to be. For example,
consider the problem of writing a program where we need to ask the user to enter their name. How
long is their name? We do not know until they enter it. It is possible to declare an array larger that we
need. Consider the code below:
#include <iostream>
int main()
{
char name[255]; // declare array large enough to hold 255 characters
std::cout << "Enter your name: ";
std::cin >> name;
std::cout << "You entered: " << name << '\n';
return 0;
}
28 | P a g e
Since we have allocated an array of 255 characters to “name”, we assume that the user will not enter
more than 255 characters. However, this is poor programming practice, because nothing is stopping the
user from entering more than 255 characters, be it by accident or maliciously.
The code below shows the recommended way of reading strings using cin:
#include <iostream>
int main()
{
char name[255]; // declare array large enough to hold 255 characters
std::cout << "Enter your name: ";
std::cin.getline(name, 255);
std::cout << "You entered: " << name << '\n';
return 0;
}
This call to cin.getline() will read up to 254 characters into name, leaving room for the null terminator.
Any excess characters will be discarded. This guarantees that the array will not overflow.
4.2 C++ Strings
In order to use strings in C++, we first need to #include the <string> header to bring in the declarations
for std::string. Once that is done, we can define variable of type std::string. Consider the code below:
#include <string>
std::string myName;
std::string myName("Alex"); // initialize myName with string literal "Alex"
myName = "John"; // assign variable myName the string literal "John"
std::string myID("45"); // "45" is not the same as integer 45!
Just like normal variables, you can initialize or assign values to strings as you would expect. Strings can
hold numbers as well. In string form, numbers are treated as text, not numbers, and thus they cannot be
manipulated as numbers (you cannot do any math with them). C++ will not automatically convert string
numbers into integers or floating point values.
Strings can be output as expected using cout. Consider the code below:
#include <string>
#include <iostream>
int main()
{
std::string myName("Alex");
29 | P a g e
std::cout << "My name is: " << myName;
return 0;
}
However, using strings with cin may yield some surprises. Consider the code below:
#include <string>
#include <iostream>
int main()
{
std::cout << "Enter your full name: ";
std::string name;
std::cin >> name; // this won't work as expected since std::cin breaks on whitespace
std::cout << "Enter your age: ";
std::string age;
std::cin >> age;
std::cout << "Your name is " << name << " and your age is " << age;
}
This produces an unexpected result. When using operator “>>” to extract a string from cin, operator
“>>” only returns characters up to the first whitespace it encounters. Any other characters are left inside
cin, waiting for the next extraction.
To read a full line of input into a string, you must use the std::getline() function instead. This function for
C++ strings takes two parameters: the first is std::cin and the second is your string variable. Consider the
code below:
#include <string>
#include <iostream>
int main()
{
std::cout << "Enter your full name: ";
std::string name;
std::getline(std::cin, name); // read a full line of text into name
std::cout << "Enter your age: ";
std::string age;
std::getline(std::cin, age); // read a full line of text into age
std::cout << "Your name is " << name << " and your age is " << age;
}
Reading inputs with both std::cin and std::getline may cause some unexpected behavior. Consider the
code below:
30 | P a g e
#include <string>
#include <iostream>
int main()
{
std::cout << "Pick 1 or 2: ";
int choice;
std::cin >> choice;
std::cout << "Now enter your name: ";
std::string name;
std::getline(std::cin, name);
std::cout << "Hello, " << name << ", you picked " << choice << '\n';
return 0;
}
You will notice when the program asks you to enter your name, it will skip waiting for you to enter your
name. It turns out, when you enter a numeric value using cin, cin not only captures the numeric value, it
always captures the newline. So when we enter ‘2’, cin actually gets the string “2\n”. It then extracts the
2 to variable “choice”, leaving the newline stuck in the input stream. Then, when std::getline goes to
read the name, it sees “\n” is already in the stream, and figures we must have entered an empty string.
A good rule of thumb is that after reading a numeric value with std::cin, remove the newline from the
stream using the std::cin.ignore() function. The corrected code can be seen below:
#include <string>
#include <iostream>
int main()
{
std::cout << "Pick 1 or 2: ";
int choice;
std::cin >> choice;
std::cin.ignore(32767, '\n'); // ignore up to 32767 characters until a \n is
removed
std::cout << "Now enter your name: ";
std::string name;
std::getline(std::cin, name);
std::cout << "Hello, " << name << ", you picked " << choice << '\n';
return 0;
}
It is possible to use the ‘+’ operator to concatenate two strings together, or the ‘+=’ operator to append
one string to another. The code below illustrates this:
31 | P a g e
#include <string>
#include <iostream>
int main()
{
std::string a("45");
std::string b("11");
std::cout << a + b << "\n"; // a and b will be appended, not added
a += " volts";
std::cout << a;
return 0;
}
Note that the ‘+’ operator concatenated the strings “45” and “11” into “4511”.
If we want to know how long a string is, we can ask the string for its length. The syntax for doing this is
different than you have seen before, but it is pretty straightforward:
#include <string>
#include <iostream>
int main()
{
std::string myName("Alex");
std::cout << myName << " has " << myName.length() << " characters\n";
return 0;
}
The length function is not a normal standalone function like we have used previously. It is a special type
of function that belongs to std::string, called a member function.
There are many other member functions of strings that can be found here:
http://www.cplusplus.com/reference/string/string/
Most of these member functions can come in handy when dealing with strings. You are expected to
know which of these member functions are to be useful, as well as their operation, for both strings and
vectors.
5. Files
File I/O in C++ works very similarly to normal I/O, with a few minor added complexities. There are 3
basic file I/O classes in C++:
32 | P a g e
ifstream: File input
ofstream: File output
fstream: Both input and output
To use the file I/O classes, you will need to #include the <fstream> header.
Unlike the cout and cin streams, which are already ready for use, file streams have to be explicitly set up
by the programmer. However, this is extremely simple: to open a file for reading and/or writing, simply
instantiate an object of the appropriate file I/O class, with the name of the file as a parameter. Then use
the insertion (<<) or extraction (>>) operator to read/write to the file. Once you are done, there are
several ways to close a file: explicitly call the close() function, or just let the file I/O variable go out of
scope.
Consider the following code below which illustrates how to read from a file (file input):
#include <iostream>
#include <fstream> //Needed to use the ifstream & ofstream functionalities
int main(){
std::ifstream infile; //input file stream operator used to handle all file‐in
functions
int num, sum=0;
infile.open("name.txt"); //opens the specified file “name.txt”
if (!infile.is_open()) //Verify whether the file exists
std::cout<<"Cannot find file"<<std::endl;
else{ //The file exists
std::cout<<"File found"<<std::endl;
while(!infile.eof()){ //execute this block until we reach the end of file
infile>>num; //read in a number from the file
sum+=num; //add the num to the sum
std::cout<<num<<std::endl; //print the number we read in from the
}
std::cout<<"Sum is "<<sum<<std::endl; //output the sum
infile.close(); //close the file
}
return 0;
}
We instructed the compiler to include the <fstream> library, which would give us use of the “ifstream”
and “ofstream” functionalities.
33 | P a g e
We declared a variable named “infile” of type “ifstream” to show that we want to read from a particular
file. We open the specific file that we want, namely “name.txt”. We expect to be reading integers from
the file. Hence, “num” and “sum” are of the integer data type.
Error handling plays an important role in programming. As programmers, there are times you are unable
to predict the behavior of your programs for specific inputs. Rather than having your program crash
unexpectedly, you can instruct your program to terminate using your own code.
If a proper file is not supplied, we do not want the program to crash. We instead inform the user of the
error that occurred and then terminate the program. We verify whether or not the file exists, using the
“infile.is_open()” function, when returns a Boolean value. If the file exists, it returns true. Otherwise, it
returns false.
A loop is utilized because we do not know how many lines there are within the file. The function
“infile.eof()” searches for the end of the file, such that the while loop will keep reading the contents of
the file until the end is reached.
We then use the “infile.close()” function to close the file stream.
Consider the following code below which illustrates how to read from a file (file input) and write to a file
(file output):
#include <iostream>
#include <fstream>
int main(){
std::ifstream infile;
std::ofstream outfile; //output file stream operator; handles all file‐out
functions
int num, sum=0;
infile.open("name.txt");
outfile.open("name_appended.txt");
if (!infile.is_open()) //Error detection & Handling
std::cout<<"Cannot find file"<<std::endl;
else{
std::cout<<"File found"<<std::endl;
while(!infile.eof()){
infile>>num;
sum+=num;
std::cout<<num<<std::endl;
}
std::cout<<"Sum is "<<sum<<std::endl;
outfile<<"Sum is "<<sum;
infile.close();
outfile.close();//Close output file
34 | P a g e
}
return 0;
}
If you search your directory, you will notice that a file “names_appended.txt” has appeared. The
function “output.open()” searches for the file named in its function parameters, within the directory. If
the file does not exist, the function creates the file.
What happens if we write to write to a file that already exists? Normally, the original file is completely
overwritten each time the program is run. What if, instead, we wanted to append some more data to
the end of the file? There are parameters that allow you to specify information about how a file should
be opened. The only parameter we are concerned with is the append parameter. Consider the code
below:
#include <iostream>
#include <fstream>
int main()
{
// We'll pass the ios:app flag to tell the ofstream to append
// rather than rewrite the file. We do not need to pass in ios::out
// because ofstream defaults to ios::out
std::ofstream outf("Sample.txt", std::ios::app);
// If we couldn't open the output file stream for writing
if (!outf)
{
// Print an error and exit
std::cout << "Uh oh, Sample.dat could not be opened for writing!" << std::endl;
return ‐1;
}
outf << "This is line 3" << std::endl;
outf << "This is line 4" << std::endl;
return 0;
}
Note that in creating “outf”, we now pass a second parameter which is “std::ios::app”. This second
parameter states that any new contents that are being written to the file are to be appended to the end
of the file, if it already exists.
35 | P a g e