Uploaded by Diana Rose Ayuda

course-notes

advertisement
Contents
Introduction
CMSC 11 Recap . . . . . . .
Operators . . . . . . .
Conditionals . . . . . .
Loops . . . . . . . . .
Preprocessor Directives
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
Functions
Definition . . . . . .
Why use functions? .
Function Prototyping
Pass-by-Value . . .
Recursion . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
6
. 6
. 8
. 9
. 10
. 11
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
3
3
3
3
4
5
Structures
15
Declaration . . . . . . . . . . . . . . . . . . . . . . . . . 16
Multidimensional Arrays
19
2D Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . 19
Matrix Math . . . . . . . . . . . . . . . . . . . . . . 21
Other uses . . . . . . . . . . . . . . . . . . . . . . . 23
Pointers
26
Arrays as Pointers . . . . . . . . . . . . . . . . . . . . . 30
Void Pointers . . . . . . . . . . . . . . . . . . . . . . . . 32
Dynamic Memory Allocation
35
Lists
38
Array Lists . . . . . . . . . . . . . . . . . . . . . . . . . 38
Vectors . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
Linked Lists . . . . . . . . . . . . . . . . . . . . . . . . . 47
File Manipulation
Writing Sequential Files . . . .
Reading Sequential Files . . .
Updating Sequential Files . . .
Writing Random-Access Files .
Reading Random-Access Files
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
58
58
61
63
64
67
C++
Input/Output . . .
Booleans . . . . .
Strings . . . . . .
References . . . .
Memory Allocation
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
69
69
70
71
72
73
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
Classes
74
Templates . . . . . . . . . . . . . . . . . . . . . . . . . 85
Exceptions . . . . . . . . . . . . . . . . . . . . . . . . . 88
1
Code Reuse
Composition . . . . . . . . . . . . . . . . . . .
Friendship . . . . . . . . . . . . . . . . . .
Inheritance . . . . . . . . . . . . . . . . . . . .
Static Members . . . . . . . . . . . . . . . . . .
Function and Operator Overloading and Overriding
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
93
93
94
96
98
101
Polymorphism
106
Interfaces . . . . . . . . . . . . . . . . . . . . . . . . . . 111
2
Introduction
Welcome to CMSC 21—Fundamentals of Programming!
This course picks up from CMSC 11 and will cover functions, arrays,
pointers, and more. Let’s begin with a brief run through of CMSC 11.
CMSC 11 Recap
Operators
CMSC 11 covered operators: = and its variants, +, -, *, /, %, ++, --.
So these should make sense:
1 int x = 5; // x = 5
2 x += 5;
// x = 10 (x = x + 5)
3 x--;
// x = 9 (x = x - 1)
4 x % 5;
// 4
5 x /= 3;
// x = 3 (x = x / 3)
Remember post-increment/decrement and pre-increment/decrement?
1 int x = 5;
2 int y = 0;
3
4 y = x++; // y = 5, x = 6
5 y = ++x; // y = 7, x = 7
6
7 y = x--; // y = 7, x = 6
8 y = --x; // y = 5, x = 5
Conditionals
If you recall, conditionals are the if statements that allow our program to branch off into different behaviors.
1 if (x > 0) {
2
printf("We're positive!");
3 } else {
4
printf("We're negative!");
5 }
3
Wherein we can combine comparators for more complex situations.
1 if (x >= 0 && x < 10) {
2
printf("Single digit territory");
3 }
4
5 if (x > 0 && (x % 2 != 0)) {
6
printf("Positive odd numbers");
7 }
Loops
Building upon comparators, we have loops.
1 while (1) {
2
printf("Running forever and ever");
3 }
4
5 do {
6
printf("Only printing this once");
7 } while (0);
8
9 for (int i = 0; i < 10; i++) {
10
printf("Printing this 10 times");
11 }
And of course, we can roll all these concepts into one.
1 for (int i = 0; i < 10; i++) {
2
printf("Printing this for the %d", i + 1);
3
if (i == 0) {
4
5
printf("st"); // Printing this for the 1st
} else if (i == 1) {
6
7
printf("nd"); // Printing this for the 2nd
} else if (i == 2) {
8
9
printf("rd"); // Printing this for the 3rd
} else {
10
printf("th"); // Printing this for the nth
11
}
12
printf(" time\n"); // ... time
4
13 }
Preprocessor Directives
To recap, a preprocessing directive shown here,
1 #include <stdio.h>
is a message to the C preprocessor. In this case, we’re telling the
preprocessor to prepend the file stdio.h to our code.
There are several other directives available like define, which substitutes each occurence of an identifier in the source file for another.
1 #define HIGHEST_GRADE 1
2
3 int gwa = HIGHEST_GRADE; // gwa = 1
There are several other directives, which you can learn more about
for your pleasure here: https://www.cprogramming.com/reference/
preprocessor/
Now let’s get to talking about functions.
5
Functions
Definition
Here is an example of a simple function in C. This function accepts
2 ints and returns the maximum.
1 int maximum(int x, int y) {
2
if (x >= y) {
3
return x;
4
} else {
5
return y;
6
}
7 }
When declaring a function, we first type out what type the function
should return.
1 int maximum(int x, int y) {
2 ^^^
3 return type
In the example above, int is our return type.
The return type is followed by the function name,
1 int maximum(int x, int y) {
2
^^^^^^^
3
function name
Function names must start with a letter or an underscore (_). After
which, it may contain alphanumeric characters or underscore.
I recommend that you name your functions descriptively and succinctly. Function names are more to help you (and future you) and
not the program.
The function name is followed by the function’s arguments.
1 int maximum(int x, int y) {
2
^^^^^^^^^^^^^^
3
arguments
6
The arguments are enclosed in parentheses, of which is composed
of an argument type and identifier (ex. int x). Arguments are separated by a comma.
After the arguments follow a set of curly braces ({}), which contain the function body. The function body defines what the function
does. In our example, it compares the 2 arguments, x and y, and
returns the bigger of the 2.
1
{
2
if (x >= y) {
3
return x;
4
} else {
5
return y;
6
}
7 }
The return keyword stops the function and passes control back to
the caller with a value (or not).
Here’s another example of a function.
1 void print_with_quotes(char* text) {
2
printf("\"%s\"", text);
3 }
Here, our return type is void, which means the function will not return a value. The function name is print_with_quotes (quite descriptive!), followed by its arguments: char* text a pointer (we
cover pointers later) to a character array called text. The function
body takes text and performs a printf(), printing text with double quotes surrounding it.
1 int main() {
2
printf("wow");
3
print_with_quotes("wow");
4 }
1 wow
2 "wow"
7
Why use functions?
Programs can get quite large and unwieldy. Imagine if all the programs you used only had the main function in it and nothing else!
Functions are a good way to structure your program into a much
more manageable mess.
Here’s a couple of reasons to use functions:
1. Divide and conquer
As you whip up more programs, you’ll come to realize that programs are just small tasks working together. It’s easier when
you can split a large problem into smaller subset of problems
and tackle those one-by-one. Ergo, divide and conquer.
Consider the task of calculating the hypotenuse of a triangle,
given the length of 2 sides. We can break down this task into
smaller parts: squaring the sides, adding them together, and
finally taking the square root of the sum.
1 float hypotenuse(float a, float b) {
2
float aSquared = square(a);
3
float bSquared = square(b);
4
float cSquared = sum(aSquared, bSquared);
5
return sqrt(cSquared); // squareroot
6 }
2. Delegation
Once you have a well written function, you should expect it to
do what it says. You can delegate the work that you have to do
to a function that you’ve already written. Write once, use many
times later.
Keeping this in mind flows well into a topic we’ll talk about later,
recursion
Consider now, the task of calculating the straight-line distance
between 2 points on the cartesian plane. As we can consider
this problem as calculating the hypotenuse of a triangle on the
plane, we can delegate that work to our function that we’ve
created (how convenient).
8
1 float distance(float x1, float y1, float x2, float y2)
{
2
float sideA = x1 - x2;
3
float sideB = y1 - y2;
4
return hypotenuse(sideA, sideB);
5 }
Function Prototyping
Below is a prototype for the hypotenuse function above.
1 float hypotenuse(float a, float b);
A function prototype describes a function without revealing its implementation. That is, there is no function body. All this tells the
compiler is that there exists a function called hypotenuse that returns a float and accepts 2 more floats, a and b.
Since the compiler reads code from top to bottom, it might come
across an identifier (in this case hypotenuse) in the code before it
knows what it means.
To demonstrate,
1 int main() {
2
return square(2);
3 }
4
5 int square(int x) {
6
return x * x;
7 }
will produce a compilation warning. Modern compilers will still continue and compile this, but older platforms/setups might not. So
might as well declare a prototype.
1 int square(int x);
2
3 int main() {
4
return square(2);
5 }
9
6
7 int square(int x) {
8
return x * x;
9 }
Note: the argument name in the prototype is optional
Pass-by-Value
Consider the following code
1 void plusOne(int x) {
2
x = x + 1;
3 }
4
5 int main() {
6
int a = 0;
7
plusOne(a);
8
return a; // a = ?
9 }
What do you think is the value of a after calling plusOne(a)?
The value of a is actually still 0. This is because the value of a is
copied into the argument x. So only the value of x inside the function
plusOne was modified.
In a way, the above scenario plays out like this.
1 int a = 0;
2 int x = a;
3 x = x + 1;
4 return a;
Notice that a wasn’t modified in the above scenario. So what if we
want to make plusOne useful? We could:
1. Make the function return the result instead, or
2. use pointers (which we’ll tackle later on)
For another example, take a look at this code.
10
1 void swap(int a, int b) {
2
int temp = a;
3
a = b;
4
b = temp;
5 }
6
7 int main() {
8
int x = 30;
9
int y = 20;
10
swap(x, y);
11 }
How about now? What is the value of both x and y? Were they
swapped?
The above code again plays out like this.
1 int x = 30;
2 int y = 20;
3 int a = x;
4 int b = y;
5 int temp = a;
6 a = b;
7 b = temp;
Recursion
Take a look at mathematical factorials, we can define it as
𝑛! = 𝑛 ⋅ (𝑛 − 1)!,
where when 𝑛 is 0, it is 1.
How fascinating, it’s defined by itself! In the same way that we can
do this in Math, we can do this in programming like so:
1 int factorial(int n) {
2
if (n == 0) {
3
return 1;
4
}
5
return n * factorial(n - 1);
11
6 }
This is what we call a recursive function, since the function calls
upon itself. We can also code up factorial like so:
1 int factorial_iterative(int n) {
2
int answer = 1;
3
while (n > 0) {
4
answer *= n--;
5
}
6
return answer;
7 }
However, the recursive function looks cleaner and cleverer looking
compared to the iterative (loop) version. It is also more faithful to
the true definition of the factorial.
A recursive function contains a base case, which is the simplest
case. In the factorial function above, the base case is when
𝑛
is
0, whose answer we know to be 1.
The rest of the recursive function typically divides, or reduces, the
problem to reach the base case. In the case of the factorial function,
we call factorial again, while reducing 𝑛 to approach the base case.
Here’s another example of a recursive function
1 int fib(int n) {
2
if (n == 0 || n == 1) {
3
return n;
4
}
5
return fib(n - 1) + fib(n - 2);
6 }
The Fibonacci sequence is formally defined as
𝐹 𝑖𝑏(0) = 0
𝐹 𝑖𝑏(1) = 1
For all integers 𝑛
> 1, 𝐹 𝑖𝑏(𝑛) = 𝐹 𝑖𝑏(𝑛 − 1) + 𝐹 𝑖𝑏(𝑛 − 2).
This definition is easily plugged into a recursive function like above.
Cool.
12
I’ll admit, recursive functions take a while to wrap your head around.
However, there are some tips to help with designing recursive functions.
Remember why we create functions? We do it for the benefit of divide and conquer and delegation of work. In the case of factorial,
we can (and should) trust that factorial(n - 1) will return the correct answer. Once we do that, we can easily find that multiplying
factorial(n - 1) by n is the right answer.
Take a look at the popular Tower of Hanoi puzzle (read on: https:
//en.wikipedia.org/wiki/Tower_of_Hanoi?oldformat=true).
Figure 1: 8-disc Tower of Hanoi puzzle
The Tower of Hanoi is a puzzle that involves 3 pegs and a number
of disks of varying sizes, which can slide into any peg. The puzzle
starts out with the disks in a stack in ascending order of size on one
rod—with the smallest disk on top. The object of the puzzle is to
move the stack to another rod, all while obeying the following rules:
1. Only one disk can be moved at a time.
2. Each move consists of taking the top-most disk from one of
the stacks and placing it sliding it on top of another stack or
empty rod.
3. No larger disk can be placed on top of a smaller disk.
The best method to solve this is via recursion, we use the concept of
delegation. To solve this puzzle, we first focus on moving the largest
disk. However, the largest disk can’t be moved because there are
smaller disks on top that have to be moved first. Ergo, we have to
move the 𝑛 − 1 smaller disks to the spare peg before we can move
the largest disk to the destination peg. In some form of pseudocode,
13
we can solve the puzzle as a function that accepts 4 arguments: the
number of disks to move, which peg to move from, which peg to
move to, and the spare peg.
1 def moveDisks(n, source, destination, spare):
2
moveDisks(n - 1, source, spare, destination)
In the code above, we trust that the function moveDisks will do what
it does. After moving the top
𝑛 − 1 disks, we can finally move the
largest disk to the destination peg:
1 def moveDisks(n, source, destination, spare):
2
moveDisks(n - 1, source, spare, destination)
3
move disk n from source to destination
Great, the largest disk is now in the right place. Now we move the
remaining disks to the destination as well:
1 def moveDisks(n, source, destination, spare):
2
moveDisks(n - 1, source, spare, destination)
3
move disk n from source to destination
4
moveDisks(n - 1, spare, destination, source)
Again, we trust that the moveDisks function will do what it says. Of
course, let us not forget the base case—which is when there are no
disks to move any more. If we reach this case, we are done and the
puzzle is solved:
1 def moveDisks(n, source, destination, spare):
2
3
4
if n == 0:
do nothing
else:
5
moveDisks(n - 1, source, spare, destination)
6
move disk n from source to destination
7
moveDisks(n - 1, spare, destination, source)
14
Structures
Do note that the functions we’ve seen so far only return a single
value, either an int, float, etc. Wouldn’t it be nice if we could return
maybe a pair of values, or maybe a couple?
Consider the function of randomizing a playing card. A playing card
consists of:
• a suit: either Hearts, Diamonds, Spades, or Clubs, and
• a value: from Ace to 10 and Jack to King.
We can encode a suit as a char in C, with 'H' for Hearts, 'D' for
Diamonds, 'S' for Spades, and 'C' for Clubs. Following this, we
can also encode the value of the card as a char as well (characters
2 - 9 for single digits, T, A, J, Q, and K for 10, Ace, Jack, Queen, and
King, respectively).
For example, if we were to encode the King of Hearts in C:
1 char suit = 'H'; // H for Hearts
2 char value = 'K'; // K for King
Conceptually, we can take 2 values: a suit and value, and pair them
up together in our head as a card. Thus, to randomize a card in C,
we could do this:
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <time.h>
4
5 char randomSuit() {
6
char suits[] = {'H', 'D', 'S', 'C'};
7
return suits[rand() % 4];
8 }
9
10 char randomValue() {
11
12
13
char value[] = {'A', '2', '3', '4', '5', '6',
'7', '8', '9', 'T', 'J', 'Q', 'K'};
return value[rand() % 13];
14 }
15
15
16 int main() {
17
srand((unsigned) time(0));
18
char suit = randomSuit();
19
char value = randomValue();
20
printf("%c%c", value, suit);
21 }
rand and srand is a C library function contained in stdlib.h.
So how do we make a function, perhaps, that does these 2 things
for us? Something like this, for example:
1 int main() {
2
Card card = randomCard();
3
printf("%c%c", card.value, card.suit);
4 }
Wouldn’t that be convenient? We can do exactly that, with the help
of structures.
A structure allows us to combine different values to form more complex data. In our example here, we combine 2 characters to form the
concept of a playing card.
Declaration
A structure is declared by having the keyword struct, followed by
the structure’s name.
1 struct Card
This is then followed by a pair of curly braces which enclose the data
that the structure is composed of and is topped off by a semicolon.
1
{
2
char value;
3
char suit;
4 };
1 struct Card {
2
char value;
16
3
char suit;
4 };
To use this structure, we can treat it as any other data type but
prepended with the keyword struct. And we can access Card’s
enclosed data with the . operator.
1 struct Card c;
2 c.value = randomValue();
3 c.suit = randomSuit();
4 printf("%c%c", c.value, c.suit);
We can do away with prepending struct to the structure name every time we use a structure by using a
typedef. We can write down typedef struct <structure
name> <structure name> to create a synonym such that
<structure name> now means struct <structure name>
Another example would be modeling the current time as a structure:
we could have 3 ints (shown below) together in a structure to represent time.
1 int hour = 22;
2 int minute = 49;
3 int second = 32;
1 struct myTime {
2
int hour;
3
int minute;
4
int second;
5 };
6 typedef struct myTime myTime;
7
8 int main() {
9
myTime t;
10
t.hour = 22;
11
t.minute = 49;
12
t.second = 32;
13 }
17
And of course, we can make structures out of structures.
1 struct myDate {
2
int month;
3
int day;
4
int year;
5 };
6 typedef struct myDate myDate;
7
8 struct Datetime {
9
myTime time;
10
myDate date;
11 };
12 typedef struct Datetime Datetime;
13
14 int main() {
15
Datetime datetime;
16
datetime.time.hour = 22;
17
datetime.date.year = 2021;
18
// etc.
19 }
Passing structures as arguments is also a thing.
1 myTime laterTime(myTime a, myTime b) {
2
int aTime = a.hour * 10000 + a.minute * 100 +
a.second;
3
int bTime = b.hour * 10000 + b.minute * 100 +
b.second;
4
if (aTime >= bTime) {
5
return a;
6
} else {
7
return b;
8
}
9 }
This function returns the later myTime.
18
Multidimensional Arrays
Just as a refresher, C arrays are declared by writing down the type
followed by its name and the size of the array in square brackets.
As an example, we declare an int array with a maximum size of 3
below.
1 int arr[3]; // an int array that can hold 3 ints
We can also initialize an array with values like so:
1 int arr[3] = {1, 2, 3};
This will initialize arr with the values 1, 2, and 3 in index 0, 1, and 2,
respectively.
When initializing an array, the size of the array becomes optional and the array size is automatically calculated from the
number of elements you initialize with.
We can access all 3 integers by using indexing. Indexing is marked
with square brackets and a positive integer index inside. Here we
store the numbers 1, 2, and 3 in our array in indices 0, 1, and 2,
respectively.
1 arr[0] = 1;
2 arr[1] = 2;
3 arr[2] = 3;
Remember that array indices start from 0 and go up to array
size - 1.
So in the same way that we can build structures with structures inside, we can also build arrays with arrays inside. This is what a multidimensional array is.
2D Arrays
2D arrays are arrays that also hold arrays. We declare them like this:
1 int twoDim[3][3]; // a 3 x 3 int array
And we can initialize a 2D array like this:
19
1 int twoDimArr[][] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
If we are to visualize how 2D arrays look, it would look like this:
[1, 2, 3],
⎡
⎤
⎢[4, 5, 6],⎥
⎣[7, 8, 9] ⎦
Using the same method as matrices in Math, we index using rows
first and then the column. The direction is also top-down and then
left-right.
Following, the value at twoDimArr[0][2] is 3 because twoDimArr[0]
refers to the first array from the top
[1, 2, 3]
and twoDimArr[0][2] is the third element in the array:
3.
Further example, twoDimArr[2][0] is equal to 7 since twoDimArr[2]
refers to the third array from the top (remember our index starts
from 0)
[7, 8, 9]
and hence, twoDimArr[2][0] is the first element of the array:
7.
2D arrays are pretty common in programming and so patterns regarding 2D arrays often come up. Below is a common pattern to
iterate over all the elements of a 2D array.
1 for (int row = 0; row < rowSize; row++) {
2
3
for (int col = 0; col < colSize; col++) {
arr[row][col];
20
4
}
5 }
This will access all the elements of the 2D array, one-by-one. Beginning from the top-left element going to the right, like reading a
book.
Matrix Math
If you haven’t yet noticed, 2D arrays behave and look awfully a lot
like matrices! In fact, these are a great tool to perform matrix math
with.
Suppose we want to add the following 2 matrices:
18 −9
2 −16
⎡
⎤ ⎡
⎤
⎢ 12 5 ⎥ + ⎢−1 2 ⎥
⎢ 8 11 ⎥ ⎢ 8
4 ⎥
⎢
⎥ ⎢
⎥
⎣−1 2 ⎦ ⎣ 21 9 ⎦
In C, we can easily model a matrix like so:
1 int A[][] = {{18, -9}, {12, 5}, {8, 11}, {-1, 2}};
2 int B[][] = {{2, -16}, {-1, 2}, {8, 4}, {21, 9}};
To perform the addition, we’ll need to iterate over all the elements
of the matrix—which we already know how to do as a pattern.
1 int rowSize = 4;
2 int colSize = 2; // since the matrix is 4 x 2
3 for (int row = 0; row < rowSize; row++) {
4
for (int col = 0; col < colSize; col++) {
5
A[row][col] + B[row][col];
6
}
7 }
The code above will perform an addition for every element, following the rules of matrix addition. The first pass of the pattern above
will access and add A[0][0] and B[0][0] together, followed by
A[0][1] and B[0][1], then A[1][0] and B[1][0], and etc.
21
Now, we have to store the result of the addition such that it is useful
to us. Let’s store it in an array called C. Make sure we declare C as
having the same size as our input matrices.
1 int C[rowSize][colSize]; // declaring an array, C, of
size 4 x 2
Now, we can store the value of the addition in C.
1 int rowSize = 4;
2 int colSize = 2; // since the matrix is 4 x 2
3 int C[rowSize][colSize]; // declaring an array, C, of
size 4 x 2
4 for (int row = 0; row < rowSize; row++) {
5
for (int col = 0; col < colSize; col++) {
6
C[row][col] = A[row][col] + B[row][col];
7
}
8 }
Cool, now C will contain the resulting matrix of the addition. We can
now print out the result, again with our pattern:
1 for (int row = 0; row < rowSize; row++) {
2
for (int col = 0; col < colSize; col++) {
3
printf("%d ", C[row][col]);
4
}
5
printf("\n"); // moving down a row, move down a
line
6 }
Further, we can do efficient matrix multiplication with arrays as well.
1 int A[2][3] = {{1, 2, 3}, {4, 5, 6}};
2 int B[3][2] = {{6, 5}, {4, 3}, {2, 1}};
Remember that matrix multiplication is only compatible between
𝑚 × 𝑛 and 𝑛 × 𝑝 matrices, resulting in an 𝑚 × 𝑝 matrix.
1 int aRow = 2;
2 int aCol = 3;
3 int bRow = 3;
4 int bCol = 2;
5 int C[aRow][bCol];
22
1 for (int row = 0; row < aRow; row++) {
2
for (int col = 0; col < bCol; col++) {
3
int c = 0;
4
for (int n = 0; n < bRow; n++) {
5
c += A[row][n] * B[n][col];
6
}
7
C[row][col] = c;
8
}
9 }
The 2 outer loops in the code above iterates over the elements in
C, so that we can store our answers. The inner loop multiplies and
adds the appropriate elements of both A and B according to the row
and column we are on.
Other uses
Other uses for 2D arrays include modeling grids and tables. For example, we can model a chessboard grid using 2D arrays.
1 char board[8][8];
2
3 for (int i = 0; i < 8; i++) {
4
board[6][i] = 'p'; // black pawn
5
board[1][i] = 'P'; // white pawn
6 }
7
8 // rest of the pieces
To be a little more consistent with chess notation (white pieces are
on rows 1 and 2), we can have our white pawns on board[1] (remember, we start at index 0). We can encode upper-case letters as
white pieces and lower-case letters as black’s pieces. Major pieces
can be the unique letters from their piece name:
• 'R' for Rook,
• 'B' for Bishop,
• 'N' for Knight,
• 'Q' for Queen, and
• 'K' for King
23
Then, to print out the board with the black pieces at the top:
1 for (int row = 7; row >= 0; row--) {
2
for (int col = 7; col >= 0; col--) {
3
printf("%c ", board[row][col]);
4
}
5 }
Take note we have to reverse our loop, starting from 7 all the way to
0 to correctly display the pieces on the board.
We can also represent tables as 2D arrays. Suppose we have a table
of monthly historical values of the PSEi by year.
Year
Month
Value
Year
Month
Value
2019
January
8,007.48
2020
January
7,200.79
2019 February
7,705.49
2020
February
6,787.91
2019
7,920.93
2020
March
5,321.23
2019 April
7,952.72
2020
April
5,700.71
2019
May
7,970.02
2020
May
5,838.84
2019 June
7,999.71
2020
June
6,207.72
2019
8,045.80 2020
July
5,928.45
2019 August
7,979.66
2020
August
5,884.18
2019
7,779.07
2020
September
5,864.23
2019 October
7,977.12
2020
October
6,324.00
2019
November
7,738.96
2020
November
6,791.46
2019 December
7,815.26
2020
December
7,139.71
March
July
September
We can store these values as a single 2D array like so:
1 float psei[2][12] = {
2
{8007.48, 7705.49, ..., 7815.26},
3
{7200.79, 6787.91, ..., 7139.71}
4 };
5 // OR
6 float psei_[12][2] = {
24
7
{8007.48, 7200.79},
8
{7705.49, 6787.91},
9
...,
10
{7815.26, 7139.71}
11 };
Then, to access the data:
1 float jan2019 = psei[0][0];
We can also create a function to make this operation more intuitive.
Of course, make the appropriate checks so we don’t get an out-ofbounds error.
1 float psei[2][12] = {
2
{8007.48, 7705.49, ..., 7815.26},
3
{7200.79, 6787.91, ..., 7139.71}
4 };
5
6 float valueAt(int year, int month) {
7
return psei[year - 2019][month - 1];
8 }
9
10 int main() {
11
valueAt(2020, 8); // PSEi value at August 2020
12 }
25
Pointers
Introducing pointers!
Pointers are a powerful tool in programming, even other programming languages have the concept of pointers.
So what are pointers?
These are data types that hold memory addresses. Just as ints
hold integers and chars hold characters, pointers hold memory addresses.
You see, variables are stored in the computer’s memory and when
we store a value, say 100, in a variable, say x, a location in memory
is reserved and the value is stored in that memory location.
The computer’s memory looks a little bit like this.
1 int x = 100;
2 int y = 200;
Figure 2: Computer memory after setting x and y
However, computers don’t deal with our variable names like x and y.
Rather, they work with memory addresses, which look a little bit like
this.
26
Figure 3: Computer memory and memory addresses
From our example above, we can say that the variable x, which contains the value 100, is located at memory address 0x6000000. In C,
we can can retrieve the memory address of the variable using the
unary address operator, &.
1 printf("%i", x); // prints out 100
2 printf("%p", &x); // prints out 0x6000000
3 printf("%i", y); // prints out 200
4 printf("%p", &y); // prints out 0x6000004
The %p format is intended for pointers and prints out memory
addresses.
Pointers hold these type of values.
We declare a pointer with an * after the type like so:
1 int* xPtr;
2 int* yPtr = 0;
3
4 printf("%p", yPtr); // prints out 0
0 is the null pointer value. This means the pointer points to
nothing. It’s a good idea to initialize pointers to the null value.
27
The position of the asterisk actually doesn’t matter, you can
have it as int* a, int * a, or int *a. When declaring multiple pointers on one line, you have to include the * everytime,
like so: int *aPtr, *bPtr, *cPtr;
Pointers can be named anything, but just by convention, we often
add Ptr to the end so we can identify that it is a pointer at a glance.
Different types and even structures can have pointers.
1 char* content;
2 Card* cardPtr;
To allow our pointers to point at something, we have to assign them
an address to point to. Remembering that the & operator retrieves
addresses, we can assign that value to our pointers. Nice!
1 int* xPtr = &x;
2 int* yPtr;
3 yPtr = &y;
4
5 char c = 'C';
6
7 char* content;
8 content = &c;
9
10 // DON'T DO THIS
11 content = &x; // assigning address of int to char*
While assigning an address of int to a char* works, I’d highly discourage it as you’ll only confuse yourself. Pointers should only hold
addresses that they are a type of.
So what can we do with pointers?
Note that pointers do not hold the value of the variable we are pointing to.
1 int x = 100;
2 int* xPtr = &x;
3
4 x == xPtr; // false
28
We can, however, get the value at the memory address pointed at
by the pointer by using the indirection operator, *.
Yes, yes, it’s unfortunately and confusingly * again. It’s different from
the * when declaring a pointer.
Using the indirection operator on a pointer will give us the value at
the memory address pointed to by the pointer. We call this dereferencing.
1 int x = 100;
2 int* xPtr = &x;
3
4 x == xPtr; // false
5 x == *xPtr; // true
So the * operator sort of reverses the & operator. Given a memory
address, * will return the value at that address. Whereas &, given a
variable, returns the memory address.
We can even update a variable through a pointer via dereferencing.
1 int x = 100;
2 int* xPtr = &x;
3 *xPtr = 200;
4 x == 200; // true
This flows well into a previous topic with functions.
Remember our plusOne function? Now we can modify it such that
the argument passed is changed using pointers:
1 void plusOne(int* x) {
2
*x = *x + 1;
3 }
4
5 int main() {
6
int a = 0;
7
plusOne(&a);
8
a == 1; // true
9 }
Since pointers don’t actually hold the value of the variable they are
29
pointing to, pointers to structures don’t have access to their properties as well.
1 struct Card {
2
char value;
3
char suit;
4 };
5 typedef struct Card Card;
6
7 int main() {
8
Card c;
9
c.value = 'K';
10
c.suit = 'H';
11
12
Card* cPtr = &c;
13
cPtr.value == 'K'; // does not work
14 }
The above doesn’t work, since it is not the pointer that has the properties value or suit. You must first dereference the pointer before
accessing its values.
1 *cPtr.value == 'K'; // now works, true
2 *cPtr.suit == 'H'; // true
Or, you can use the arrow operator, ->.
1 cPtr->value == 'K'; // 'K'
2 cPtr->suit == 'H'; // 'H'
The arrow operator is equivalent to dereferencing the pointer and
performing the dot operator in one.
Arrays as Pointers
In C, built-in arrays are implicit pointers. That is, the name of an array
is actually a pointer to the first element.
1 int arr[3] = {1, 2, 3};
2
3 arr == &arr[0]; // true
4
30
5 *arr == 1; // true
Notice that we can dereference (*) the name of the array to get the
first element. How do we get the rest of the elements?
Pointer arithmetic is the answer.
Take another look at the following:
1 int arr[] = {100, 200};
Figure 4: Computer memory and memory addresses
Notice that the address of x is exactly 4 units away from y. That’s
because in this example, an int takes up 4 bytes. The size of an
int, however, is platform dependent, so it might not be 4 bytes for
your computer.
In conventional arithmetic, 3000 + 2 would be 3002. However, with
pointer arithmetic, adding an integer to a pointer would add that
integer times the size of the memory object to which the pointer
refers to. Hence, adding the integer 2 to our example int pointer
would add 2 × 4
= 8 units.
1 int arr[] = {100, 200, 300, 400};
2 arr; // 0x6000000
3 arr + 1; // 0x6000004
4 *(arr + 1) == arr[1]; // true
5 arr + 3; // 0x6000000c
6 *(arr + 3) == arr[3]; // true
Hence, we can do this:
31
1 int sum(int* arr, int n) {
2
int ans = 0;
3
for (int i = 0; i < n; i++) {
4
ans += *(arr + i);
5
}
6
return ans;
7 }
And, we can also do this:
1 int sum(int* arr, int n) {
2
int ans = 0;
3
for (int i = 0; i < n; i++) {
4
ans += arr[i];
5
}
6
return ans;
7 }
That’s right, we can treat the arr pointer as an array—since we know
that it is an array. Under the hood, the statement arr[i] actually
equates to *(arr + i).
Note: Pointer arithmetic only works for arrays treated as pointers.
To be honest, there’s not much benefit to using pointer arithmetic
over the more intuitive indexing. Might as well just use indexing
instead of performing pointer arithmetic as it’s easier to write and
understand.
As a side note, since arrays are technically pointers, we can use a
char* for C-style strings as well.
1 char* content = "This is a string";
Void Pointers
Void pointers are declared, you guessed it, with void*. Void pointers
are pointers to any type in C. They are general purpose pointers and
thus can hold pointers to any type.
32
1 int x = 100;
2 void* pointer;
3
4 pointer = &x;
Void pointers are quite useful to hold pointers to types that we don’t
know at the time of writing. They will come quite useful in later chapters as well.
They work just like any pointer, but we can’t immediately perform
dereferencing on a void pointer just yet. Since dereferencing a void
pointer would give us a void type, and that doesn’t quite make sense.
1 int x = 100;
2 void* pointer = &x;
3
4 *pointer == 100; // comparing void to int, illegal
To dereference a void pointer we first have to cast it into the type
we want.
We can perform a type cast with the following syntax:
(<data type to cast into>)<item to cast>.
1 int* intPtr = (int*) pointer;
2 *intPtr == 100; // valid and true!
Casting a value is to treat it as the type you are casting the value
to. In the example above, (int*)pointer casts the void pointer,
pointer, into the mold of an int*.
As another example of casting, we can cast floats or doubles to
ints.
1 float x = 2.5;
2 int x_int_cast = (int) x;
3
4 x == 2; // false
5 x_int_cast == 2; // true
Since ints don’t have decimals, they are dropped when 2.5 is
casted to an int.
33
Another interesting case is for casting chars to int.
1 int ascii = (int) 'A';
2 ascii == 65; // true
That’s right, casting a char to int gives us its ASCII code.
34
Dynamic Memory Allocation
All the memory allocations we’ve seen so far come from the memory stack. Whenever we declare a variable, some memory from the
stack is reserved and is given to us.
This memory is kept reserved until the variable falls out of scope.
1 int main() {
2
if (1) {
3
int inScope = 5;
4
}
5
inScope == 5; // error: 'inScope' undeclared
6 }
Since inScope was declared inside the if block, it only exists inside
the if block and ceases to exist outside of it. If we wanted to keep
inScope after the if block, we have to declare it outside of the if
block.
1 int main() {
2
int inScope;
3
if (1) {
4
inScope = 5;
5
}
6
inScope == 5; // true
7 }
A rule of thumb for scope markers are the curly braces. A set of curly
braces usually indicate a new scope. Variable declarations are only
available in their current scope and deeper.
Now, consider the following:
1 Card* randomCard() {
2
Card c;
3
c.value = randomValue();
4
c.suit = randomSuit();
5
return &c;
6 }
7
8 int main() {
35
9
Card* cPtr = randomCard();
10
printf("%c%c", cPtr->value, cPtr->suit);
11 }
What do you think will happen?
We actually get a segmentation fault. That’s because Card c only
exists in randomCard() and actually falls out of scope when we return to main. The memory address returned to cPtr hence does not
belong to Card c any longer and would be invalid.
We can remedy this with dynamic memory allocation. Dynamically
allocated memory is taken from the heap and does not get cleaned
up when we exit a scope. It becomes our (your) burden to free or
return the memory after we’re done with it.
The heap is a region of memory in the computer for storing dynamically allocated objects.
We can dynamically allocate memory with the malloc() function in
C. This allows us to reserve memory from the heap (and not from
the stack). Thus allowing the memory to persist, even after leaving
its scope.
1 Card* randomCard() {
2
Card* c = (Card*) malloc(sizeof(Card));
3
c->value = randomValue();
4
c->suit = randomSuit();
5
return c;
6 }
malloc() accepts an int that tells the computer how much memory
we need. It then returns the address of the requested memory as a
void pointer. The sizeof operator returns the size of the operand in
bytes. Since data types in C vary depending on the platform, it’s a
good idea to use the sizeof operator to make our program work on
different platforms.
In the code above, we are requesting the heap for enough memory
for a Card type. We then cast the returned void pointer to a Card*,
as is appropriate.
36
Since we manually allocated this memory, we also have to manually
free or return it to the heap. Not doing so might result in our program
running out of available memory—we call this a memory leak.
To free memory, we use the free() function.
1 int main() {
2
Card *card = randomCard();
3
printf("%c%c", card->value, card->suit);
4
// we're done playing with `card`
5
free(card);
6 }
As another example, we allocate memory for an array. The benefit
here is that we can dynamically resize the array when needed.
1 int* arr = (int*) malloc(sizeof(int) * 20); //
allocate for 20 ints
When we have to resize later:
1 free(arr); // don't forget to free
2 arr = (int*) malloc(sizeof(int) * newSize);
Under static allocation of arrays, we are unable to resize the array
after declaration.
Of course, when we do free(arr) we lose the information stored in
the array. We can first allocate memory for the new array, copy the
data over, and then free the old array after.
This flows into the next topic: Lists.
37
Lists
Plenty of applications are basically lists and lists of information.
News sites? List of articles.
FB News feed? List of posts.
Twitter Timeline? List of tweets.
You’d be hard pressed to find an application that doesn’t make use
of a list. So lists are pretty important to programming.
There are multiple implementations of lists, which we’ll go through
here. Let’s first start off where we left off: arrays.
Array Lists
You should be quite familiar with array lists already. We simply use
plain arrays. But here we sort of “formalize” the list and create functions to help us use the list properly.
For now, let us design a list that holds positive integers. It’s important
that we hold only positive integers so that our functions can return
negative numbers to indicate an invalid value. We’ll tackle how to
deal with invalid values later.
1 struct myList {
2
int size;
3
int arr[20];
4 };
5 typedef struct myList myList;
The size stores the number of elements already in our list, while arr
is an array that stores the actual elements of our list.
Then we also define some operations that we can do on our list.
1 void initList(myList* list);
2 int getItem(myList* list, int index);
3 void setItem(myList* list, int index, int item);
4 void insertItem(myList* list, int index, int item);
5 int removeItem(myList* list, int index);
38
The initList(myList*) function accepts a pointer to a list and initializes it.
1 void initList(myList* list) {
2
list->size = 0;
3
for (int i = 0; i < 20; i++) {
4
list->arr[i] = -1;
5
}
6 }
Figure 5: Our list after initialization
We set list->size to 0 as our list is empty in the beginning. We
also set the elements of list->arr to -1 as an initial value, which
means no value is set here.
The getItem(myList*, int) function allows us to get the item located at the specified index. This is as simple as directly indexing
the array, arr.
1 int getItem(myList* list, int index) {
2
return list->arr[index];
3 }
However, we should also check that this is actually an int that we
inserted and not just an initial value. So we check whether the index
is valid by checking whether it is in between 0 and size - 1.
1 int getItem(myList* list, int index) {
2
if (index >= 0 && index <= list->size - 1) {
3
return list->arr[index];
4
}
5
return -1; // return invalid value
39
6 }
The setItem(myList*, int) allows us to set the element at the
specified index.
1 void setItem(myList* list, int index, int item) {
2
list->arr[index] = item;
3 }
Again, we should also check whether this is a valid index of 0 to
list->size - 1.
1 void setItem(myList* list, int index, int item) {
2
if (index >= 0 && index <= list->size - 1) {
3
list->arr[index] = item;
4
}
5
return; // invalid index, do nothing
6 }
The insertItem(myList*, int, int) item allows us to insert
items into the list at the specified index. That is, when we’re doine
inserting an item at index i, for instance, getItem(list, i) will
give us the inserted item.
1 void insertItem(myList* list, int index, int item) {
2
if (index >= 0 && index <= list->size) {
3
list->arr[index] = item;
4
list->size++;
5
}
6
return;
7 }
But wait, what if an element already exists at the specified index?
That would overwrite the element and we’d be losing data! We’ll
have to move some list elements aside.
We can move the elements from the specified index up until
list->size - 1 one space to the right to make space for our new
item. The following snippet does that for us:
1 for (int i = list->size; i > index; i--) {
2
list->arr[i] = list->arr[i - 1];
40
3 }
Figure 6: Shifting elements in the array
1 void insertItem(myList* list, int index, int item) {
2
if (index >= 0 && index <= list->size) {
3
for (int i = list->size; i > index; i--) {
4
list->arr[i] = list->arr[i - 1];
5
}
6
list->arr[index] = item;
7
list->size++;
8
}
9
return;
10 }
You might notice that we might step out of bounds while shifting
elements in the array. Or you might have noticed already that our
array only has space for 20 items. What if we were to add more?
This is a limitation of the array list, but fret not, there’s a solution
which we’ll look at next. For now, let’s cover that case:
1 if (list->size == 20) {
41
2
return;
3 }
1 void insertItem(myList* list, int index, int item) {
2
if (list->size == 20) {
3
return;
4
}
5
if (index >= 0 && index <= list->size) {
6
for (int i = list->size; i > index; i--) {
7
list->arr[i] = list->arr[i - 1];
8
}
9
list->arr[index] = item;
10
list->size++;
11
}
12
return;
13 }
Finally, the removeItem(myList*, int) allows us to remove the
item located at the specified index.
This involves the reverse
of insertItem(myList*, int, int) in that we have to move
elements around to fill in the gap left by the removed item.
This snippet does just that:
1 for (int i = index; i < list->size - 1; i++) {
2
list->arr[i] = list->arr[i + 1];
3 }
42
Figure 7: Moving elements to take place of removed element
1 void removeItem(myList* list, int index) {
2
if (index >= 0 && index <= list->size - 1) {
3
for (int i = index; i < list->size - 1; i++) {
4
list->arr[i] = list->arr[i + 1];
5
}
6
list->size--;
7
}
8 }
You might notice that an element may be duplicated by the shifting,
but not to worry, the element is effectively deleted because of the
size decrement.
Vectors
Remember how our array list can only fit as much as the declared
array? What if we wanted to store more than 20, for instance? We
technically can declare a bigger array like so:
1 struct myList {
43
2
int size;
3
int arr[500];
4 };
No way we’d insert more than 500 right? If we do, we just declare
an even larger array instead, right?
Well yes, we could, but what if we don’t use 500? What if our application only uses 5 items then? Allocating for 500 items would be a
total waste of space, as we don’t use nearly as much. We’re trying
to conserve resources here.
Well, a solution is to resize the array when we reach the array’s capacity. This is what vectors do. Vectors are containers that represent
arrays that can change in size.
1 struct myVector {
2
int size;
3
int capacity;
4
int arr*;
5 };
6 typedef struct myVector myVector;
Note that we make use of an int* instead of declaring a fixed-size
array. We also have an int to keep track of the capacity of the
current allocated array.
Our vector will also implement the same functions as our array list
above.
1 void initList(myVector* list);
2 int getItem(myVector* list, int index);
3 void setItem(myVector* list, int index, int item);
4 void insertItem(myVector* list, int index, int item);
5 int removeItem(myVector* list, int index);
For our list initialization, since we are resizing the array, we have to
use the heap and allocate memory with malloc. Initially, we can set
the capacity to a reasonable size of 10. We can resize it later down
the road.
1 void initList(myVector* list) {
44
2
list->size = 0;
3
list->capacity = 10;
4
list->arr = (int*) malloc(sizeof(int) *
list->capacity);
5 }
Then, the implementation of get and set item functions are quite
exactly the same. The differences lie where we add or remove items.
For insertItem(myVector*, int, int), we check whether we’d
be overshooting our capacity and resize the array, instead of giving
up.
1 // already at capacity, +1 would overshoot
2 if (list->size == list->capacity) {
3
resize(list);
4 }
The resize(myVector*) function will resize the capacity of the array to double of list->size. Do note that resizing the array takes
some time, and that doubling the array when we reach capacity is a
good balance of how often we have to perform resizes.
1 void resize(myVector* list) {
2
int newCapacity = 2 * list->size;
3
int* newArr = (int*) malloc(sizeof(int) *
newCapacity);
4
for (int i = 0; i < list->size; i++) {
5
newArr[i] = list->arr[i];
6
}
7
list->capacity = newCapacity;
8
free(list->arr);
9
list->arr = newArr;
10 }
45
Figure 8: Array resize
Of course, don’t forget to free the old array!
1 void insertItem(myVector* list, int index, int item) {
2
if (index >= 0 && index <= list->size) {
3
if (list->size == list->capacity) {
4
resize(list);
5
}
6
for (int i = list->size; i > index; i--) {
7
list->arr[i] = list->arr[i - 1];
8
}
9
list->arr[index] = item;
10
list->size++;
11
}
12
return;
13 }
That’s pretty much the only change!
For removeItem(myVector*, int), we can resize the array when
our list->size drops below a third of our capacity—again for a
good balance.
1 if (list->capacity / 3 < list->size - 1) {
2
resize(list);
3 }
1 int removeItem(myVector* list, int index) {
46
2
if (index >= 0 && index <= list->size - 1) {
3
if (list->capacity / 3 < list->size - 1) {
4
resize(list);
5
}
6
for (int i = index; i < list->size - 1; i++) {
7
list->arr[i] = list->arr[i + 1];
8
}
9
list->size--;
10
}
11 }
Cool, we’ve implemented a list that grows or shrinks as need be. It
solves all our problems with the array list!
But notice how right after we resize the array, there’s a lot of wasted
space? Whenever we resize, we are increasing the capacity of the
array to double its size. That means there are list->size spaces
in the array that is not in use.
Again, we have a solution!
Linked Lists
Linked lists are containers just like the previous lists, except we are
more conservative about allocating memory. We don’t bulk allocate
memory like in the case of vectors. Rather, we allocate every time
we insert a new item.
This time, we don’t use C’s arrays. We’ll create our own structure to
hold the data, which will make heavy use of pointers.
We’ll first introduce that structure that will compose our linked list.
1 struct myNode {
2
int value;
3
struct myNode* next;
4 };
5 typedef struct myNode myNode;
47
Figure 9: VIsual representation of a node
This is what we call a node. The node stores the data itself and often
links to other nodes that are part of the collection via pointers. This
one only has a link to 1 other node, which we typically call next.
We’ll first introduce a linked list that is composed of nodes with only
a single pointer, a pointer to the next element of the sequence. This
is what we call a singly-linked list.
1 struct SLList {
2
myNode* head;
3
myNode* tail;
4
int size;
5 };
6 typedef struct SLList SLList;
Our SLList uses 2 variables, head and tail to keep track of the
first and last node in sequence. We also keep track of the number
of elements in the list, size.
To make things easier, we’ll also implement functions to help us use
this structure.
1 void initList(SLList* list);
2 int getItem(SLList* list, int index);
3 void setItem(SLList* list, int index, int item);
4 void insertItem(SLList* list, int index, int item);
5 int removeItem(SLList* list, int index);
48
We first initialize the list by setting the pointers to the null pointer (0)
and the size to 0.
1 void initList(SLList* list) {
2
list->head = 0;
3
list->tail = 0;
4
list->size = 0;
5 }
To retrieve an item from the list, we should follow the next pointers
until we reach the specific one. That’s right, we can’t simply use an
index like in vectors.
We can implement a helper function to facilitate following the links
along.
1 myNode* getNode(SLList* list, int index) {
2
myNode* current = list->head;
3
for (int i = 0; i < index; i++) {
4
current = current->next;
5
}
6
return current;
7 }
The function above follows the next link as many times as the specified index, which will land us on the desired node.
With this function, the getItem(SLList* list, int index) and
setItem(SLList* list, int index) become trivial.
1 int getItem(SLList* list, int index) {
2
if (index >= 0 && index <= list->size - 1) {
3
myNode* node = getNode(list, index);
4
return node->value;
5
}
6
return -1;
7 }
8
9 void setItem(SLList* list, int index, int item) {
10
if (index >= 0 && index <= list->size - 1) {
11
myNode* node = getNode(list, index);
12
node->value = item;
49
13
}
14
return;
15 }
To insert an item into our list, we’ll have to allocate memory for a new
node to hold our item. We’ll also have to make a function for that as
well.
1 myNode* createNode(int x) {
2
myNode* a = (myNode*) malloc(sizeof(myNode));
3
a->value = x;
4
a->next = 0;
5
return a;
6 }
This function allocates a node with a preset value of x.
With that function out of the way, we can implement our insert.
1 void insertItem(SLList* list, int index, int item) {
2
if (index >= 0 && index <= list->size) {
3
myNode* toInsert = createNode(item);
4
if (index == 0) {
5
toInsert->next = list->head;
6
list->head = toInsert;
7
}
8
if (index == list->size) {
9
if (list->tail != 0) {
10
list->tail->next = toInsert;
11
}
12
list->tail = toInsert;
13
}
14
if (index > 0 && index < list->size) {
15
myNode* previous = getNode(list, index 1);
16
toInsert->next = previous->next;
17
previous->next = toInsert;
18
}
19
list->size++;
20
}
50
21 }
Let’s break this down.
1 myNode* toInsert = createNode(item);
This creates a node with the value of item.
For instance, 5.
Figure 10: A new node with value of 5
1 if (index == 0) {
2
toInsert->next = list->head;
3
list->head = toInsert;
4 }
51
Figure 11: Inserting the value 5 at the head of the linked list
If we’re inserting at the beginning of the list (index 0), we point
toInsert->next to the current list head and update the list head
to point to our new node.
1 if (index == list->size) {
2
if (list->tail != 0) {
3
list->tail->next = toInsert;
4
}
5
list->tail = toInsert;
6 }
52
Figure 12: Inserting the value 5 at the tail of the linked list
If it happens that we’re inserting at the tail, we update the current
tail->next, if it exists, to point to the the new node and update the
tail.
1 if (index > 0 && index < list->size) {
2
myNode* previous = getNode(list, index - 1);
3
toInsert->next = previous->next;
4
previous->next = toInsert;
5 }
If we’re inserting in the middle of the list (not the head and not the
tail), we get the node previous to the one we want. This is so we can
update the next pointers to point to our new node.
53
Figure 13: Inserting the value 5 at index 1 of the linked list
And finally, don’t forget to increase the size:
1 list->size++;
The remove function can be implemented like so:
1 int removeItem(SLList* list, int index) {
2
if (index >= 0 && index <= list->size - 1) {
3
myNode* toRemove;
4
if (index == 0) {
5
toRemove = list->head;
6
list->head = toRemove->next;
7
8
} else {
myNode* previous = getNode(list, index 1);
9
toRemove = previous->next;
10
previous->next = toRemove->next;
11
if (index == list->size - 1) {
54
12
list->tail = previous;
13
}
14
}
15
free(toRemove);
16
list->size--;
17
}
18 }
Breaking it down:
1 if (index == 0) {
2
toRemove = list->head;
3
list->head = toRemove->next;
4 }
Figure 14: Removing the head from a singly-linked list
If we’re removing from the head, we store the head in the toRemove
pointer and update the head to toRemove->next.
1 else {
2
myNode* previous = getNode(list, index - 1);
3
toRemove = previous->next;
4
previous->next = toRemove->next;
5
6 }
55
Figure 15: Removing an element that is not the head from a singlylinked list
Else, we again get the node previous to what we want so that we are
able to update its next pointer. We set toRemove to the actual node
we want to remove and update the previous->next pointer to skip
the toRemove node.
1 if (index == list->size - 1) {
2
list->tail = previous;
3 }
Figure 16: Updating the tail
56
If the node we are removing happens to be the tail, we update the
tail now.
1 free(toRemove);
Don’t forget to free!
1 list->size--;
Again, don’t forget to update the list size.
That’s pretty much it for implementing the SLList. We can improve
upon this with a doubly-linked list, which builds upon the singlylinked list by having a previous pointer like so:
1 struct myNode {
2
int value;
3
struct myNode* next;
4
struct myNode* prev;
5 };
Of course, the operations will have to update these pointers appropriately.
Now you might notice that we’ve only handled ints, and only positive ones at that. We’ll fix that later, when we learn about C’s successor: C++.
We can now use our lists like so:
1 SLList scores;
2 initList(&scores);
3 insertItem(&scores, scores.size, 100);
4 insertItem(&scores, scores.size, 200);
5 insertItem(&scores, scores.size, 300);
6 insertItem(&scores, scores.size, 400);
7 getItem(&scores, scores.size - 1) == 400; // true
8 getItem(&scores, 1) == 200; // true
9 removeItem(&scores, 2) == 300; // true, removes
10 scores.size == 3; // true
57
File Manipulation
Oh now we’re getting to more interesting parts.
File manipulation is a core functionality to provide programs some
persistence. Persistence is what makes most applications useful.
We’ve already been working with memory, but that kind of memory
is lost when the program ends. Persistence allows the memory to
persist beyond program end, and allows us to reuse that memory for
later runs of the program.
Without persistence, nothing you do on a computer would be saved
after a shut down. So persistence is quite an important functionality.
C views files as a sequence of bytes sort of like an array of bytes,
and similar to how C-style strings (which are character arrays) end
with a null character (\0), all files on your computer should end with
an end-of-file marker (EOF).
Writing Sequential Files
To start off, let’s write a sequential file using C.
We’ll write a little program that accepts 3 things: an account number,
account name, and its balance. With these 3 things, we can create
a simple accounting system.
We’ll be including the stdio.h header as it includes definitions we
need for file processing.
1 #include<stdio.h>
2
3 int main() {
4
FILE* fp = fopen("data.txt", "w");
5
if (!fp) {
6
printf("Oh no, an error occurred!\n");
7
return -1;
8
}
9
10
printf("Enter account number, name, and
balance.\nEnter EOF to end input.\n? ");
58
11
12
int account;
13
char name[21];
14
double balance;
15
16
while(scanf("%i %s %lf", &account, name, &balance)
!= EOF) {
17
fprintf(fp, "%i %s %lf\n", account, name,
balance);
18
printf("? ");
19
}
20
21
fclose(fp);
22 }
1 Enter account number, name, and balance.
2 Enter EOF to end input.
3 ? 100 Rex 2.5
4 ? 200 John 300
5 ? 300 Mitch 125.23
6 ?
The above program continually accepts the 3 items and writes it to
a file called data.txt until it meets an EOF marker. Let’s break this
down further:
1 FILE* fp = fopen("data.txt", "w");
This opens a file called data.txt with the write mode. There are
several modes available, such as:
• "r" for read, reads from start, fails if file doesn’t exist.
• "w" for write, destroys contents, creates file if it doesn’t exist.
• "a" for append, writes at end (append), creates file if it doesn’t
exist.
• "r+" for read extended, just like "r" but allows writing, writing
starts at the top.
• "w+" for write extended, just like "w" but allows reading.
• "a+" for append extended, just like "a" but allows reading.
For more details, see: https://en.cppreference.com/w/c/io/fopen
59
Note: Be careful, write mode will remove the specified file’s
contents without warning.
The following checks if we opened the file successfully. If fopen
returns NULL, that means our file wasn’t opened successfully and
we should exit.
1 if (!fp) {
2
printf("Oh no, an error occurred!\n");
3
return -1;
4 }
The next few lines simply declare some variables for our program to
play with, so we’ll skip to the user input loop.
1 while(scanf("%i %s %lf", &account, name, &balance) !=
EOF) {
2
fprintf(fp, "%i %s %lf\n", account, name, balance);
3
printf("? ");
4 }
This will continually accept user input in the format of "%i %s %lf",
which corresponds to our account number, account name, and balance. These 3 items are separated by spaces.
Do note that our account name cannot have spaces and is only limited to 20 characters.
Inside the loop, we write the account number, name, and balance
followed by a new line to the file. Afterwhich we prompt for another
entry.
When we enter an EOF marker, with <Ctrl-D> on Linux and
<Ctrl-Z> + <Enter> on Windows, we take that as a sign that no
more entries are to be entered and to stop looping.
1 fclose(fp);
This closes the file represented by fp, which should now contain our
account entries. Go on, check it out.
60
Tip: Close files with fclose as soon as it’s not longer needed
in the program.
Success, we’ve written some data to a file!
Reading Sequential Files
Now, it’s time to use it. Let’s learn how to read back from a file for
use in the program.
Most of it is actually similar to file writing and we’ll just flip the functions we use.
1 FILE* fp = fopen("data.txt", "r");
2 if (!fp) {
3
printf("Oh no, an error occurred!\n");
4
return -1;
5 }
6
7 int account;
8 char name[21];
9 double balance;
10
11 printf("Account No.
Name
Balance\n");
12 while(fscanf(fp, "%i %20s %lf", &account, name,
&balance) != EOF) {
13
printf("%i %s %lf\n", account, name, balance);
14 }
15
16 fclose(fp);
1 Account No.
Name
Balance
2 100 Rex 2.500000
3 200 John 300.000000
4 300 Mitch 125.230000
Going through the few notable differences, we used the “read” mode
to open the file instead of the “write” mode:
1 FILE* fp = fopen("data.txt", "r");
61
And we used an fscanf to read from the file in the same format as
used in the writing with fprintf, instead of asking for user input.
We keep reading from the file until we reach an EOF marker. While
we read each line from the file, we are printing it out with the printf
function. Shown here:
1 while(fscanf(fp, "%i %s %lf", &account, name,
&balance) != EOF) {
2
printf("%i %s %lf\n", account, name, balance);
3 }
Your read program should print out the entries that we created with
the write program.
We normally read files sequentially, from beginning of the file until
the desired data is found, or we reach EOF. However, it might be
necessary to process the file several times.
To do so, we don’t have to close the file and reopen it, we can use a
function that repositions the file-position pointer.
Yes, files opened with fopen have a file-position pointer associated
with them. This pointer keeps track of which byte number in the
file at which the next input/output should be placed. This is how
succeeding calls to fscanf keep track of the next line and so on.
We can get the current file-position with the ftell function:
1 long pos = ftell(fp);
which we can pass to the fseek function.
The fseek function repositions the file-position pointer of a FILE*.
The prototype for the fseek function is as follows:
1 int fseek(FILE* stream, long offset, int origin);
We can use this function like so:
1 FILE* fp = fopen("data.txt", "r");
2 if (!fp) {
3
return -1;
4 }
5
62
6 int x;
7 fscanf(fp, "%i", &x);
8 fseek(fp, 0, SEEK_SET);
9 fscanf(fp, "%i", &x);
The statement
1 fseek(fp, 0, SEEK_SET);
repositions the file-position pointer to 0, relative to SEEK_SET.
The value of offset moves the file-position pointer that much relative
to the following origins that are available: - SEEK_SET, beginning of
the file, - SEEK_CUR, current position, and - SEEK_END, from the end
of the file.
Read more here: https://en.cppreference.com/w/c/io/fseek
Updating Sequential Files
Updating data in sequential files can get a bit tricky. This is because
there is a risk of destorying other data in the file.
Consider, for example, that files are arrays:
Figure 17: File as array
When we update the text “Hello” in the file to “Goodbye” for instance,
63
Figure 18: Updating sequential files
We destroy the rest of the file, which isn’t good.
The problem is that fields can vary in size. For instance the values
1, -255, 1024, and -215782 can all fit in 4 bytes internally. However,
when written to a file, they are converted to characters and thus
occupy different sizes depending on their values.
We technically can update a sequential file by copying data to a new
file, writing the updated record, then continue copying the rest. For
our "Hello\nWorld!" example, we can start writing "Goodbye" to
a new file then start seeking in the file until we see the end of the
word "Hello", wherein we start copying over the old data.
This is awkward and is resource intensive as it requires processing
all the data in the file. We can improve upon this by writing randomaccess files instead of sequential files.
Writing Random-Access Files
Random-access files allow instant access to certain parts of a file.
That is, we don’t have to scan a file from the top and sequentially
read until we reach the wanted line.
Many techniques to implement random-access files exist, but the
easiest method is to require that all records in a file be of the same
fixed length. This allows us to calculate the exact location of any
record in a file.
For instance, in our simple accounting program, we can require that
each entry be 100 bytes each. Therefore, reading a file at the 300th
64
byte would give us the 3rd entry.
Here we are able to update data without risk of destroying others.
As we know which range of bytes belong to a record, we can simply
overwrite that specific range to overwrite a single record.
Below is the same simple accounting program, but with file writing
changed to enable random-access:
1 #include<stdio.h>
2
3 struct Record {
4
int account;
5
char name[21];
6
double balance;
7 };
8 typedef struct Record Record;
9
10 int main() {
11
FILE* fp = fopen("data.bin", "wb");
12
if (!fp) {
13
printf("Oh no, an error occurred!\n");
14
return -1;
15
}
16
17
printf("Enter account number 1 - 100 (0 to
exit)\n? ");
18
19
Record r;
20
21
scanf("%i", &r.account);
22
while(r.account > 0 && r.account <= 100) {
23
printf("Enter name and balance\n? ");
24
scanf("%s %lf", r.name, &r.balance);
25
fseek(fp, sizeof(Record) * (r.account - 1),
SEEK_SET);
26
fwrite(&r, sizeof(Record), 1, fp);
27
printf("Enter account number 1 - 100 (0 to
exit)\n? ");
65
28
scanf("%i", &r.account);
29
}
30
31
fclose(fp);
32 }
Breaking this down:
1 struct Record {
2
int account;
3
char name[21];
4
double balance;
5 };
We create a struct to hold the data which we’ll write down to the
file. A C structure has a fixed-length size and so is a good tool for
writing random-access files.
1 FILE* fp = fopen("data.bin", "wb");
Next, we open a file called “data.bin” with the “write binary” mode.
The binary mode turns off special handling of characters, which allows us to directly write data into the file.
1 while(r.account > 0 && r.account <= 100) {
2
printf("Enter name and balance\n? ");
3
scanf("%s %lf", r.name, &r.balance);
4
fseek(fp, sizeof(Record) * (r.account - 1),
SEEK_SET);
5
fwrite(&r, sizeof(Record), 1, fp);
6
printf("Enter account number 1 - 100 (0 to
exit)\n? ");
7
scanf("%i", &r.account);
8 }
This time we limit the account number from 1 to 100 simply for
demonstration.
Given a valid account number, along with a name and balance, we
use the fseek function to jump to a position in the file that would
correspond to its account number.
66
We calculate for the byte location of the record by subtracting 1
from the account number and multiplying it by the size of the record.
Thus, assuming a record is 10 bytes long, account 0 is found at byte
0, account 1 is found at byte 10, account 20 is at byte 200, and so
on.
After seeking to the proper byte location, we write the record with
the fwrite function.
The fwrite function allows us to write binary data to the file. The
prototype of the fwrite function is as follows:
1 size_t fwrite(const void *buffer, size_t size, size_t
count, FILE* stream);
Given an address of the object to write, buffer, the size of the object, size, the number of objects to write, count, and the file pointer,
stream, we are able to persist an object to a file.
If you take a look at data.bin with a text editor, it might look like a
lot of jumbled text.
Doesn’t look like much, but that’s because your text editor is trying
to display binary data as characters. Your computer just doesn’t
know how to display these as characters. And since your computer
does know how to display actual characters, only the name portion
of each record is human-readable.
Reading Random-Access Files
First to demonstrate that those jumbled characters actually contain
our records, a simple program to read back records from the written
file:
1 FILE* fp = fopen("data.bin", "rb");
2 if (!fp) {
3
printf("Oh no, an error occurred!\n");
4
return -1;
5 }
6
7 Record r;
8
67
9 printf("Account No.
Name
Balance\n");
10 while(fread(&r, sizeof(Record), 1, fp) && !feof(fp)) {
11
if (r.account != 0) {
12
printf("%i %s %lf\n", r.account, r.name,
r.balance);
13
}
14 }
15
16 fclose(fp);
Note that we use the "rb" mode, for “read binary”. We also use the
same Record structure we used to write the file as we use to read
back from the file.
This time we use the fread function, which is essentially the opposite of the fwrite function. We also check whether we’ve reached
EOF with feof.
For each supposed entry in the file, we check whether it’s a valid
account (not 0) and print valid accounts out.
Your records should be printed out as a result.
We can retrieve a single record by account number efficiently as well.
All we have to do is calculate for the byte location of the record in
the file, which we can do with fseek.
1 fseek(fp, sizeof(Record) * (accountNumber - 1),
SEEK_SET);
This seeks to the exact record we want and all we have to do is to
perform an fread.
1 fread(&record, sizeof(Record), 1, fp);
Ain’t that cool? It’s as if we’re treating a file as an array, with the
account numbers as the index, and actually that’s a good way to
think about it.
With random-access files we can also update objects safely and efficiently. We simply seek to the byte location of the desired object
and overwrite with fwrite.
68
C++
Now that we’ve learned much about C, let’s learn about a more powerful tool, C++.
C++ is a language that was built to be an increment of C, hence the
++. It brings with it many quality of life changes for developers and
the most notable improvement is the introduction of classes.
But before we talk about what classes are, let’s first talk about the
subtle differences C++ has over C.
Input/Output
For one, the printf, scanf, and stdio.h is no more. In C++, we use
cin, cout, and iostream.
1 #include<iostream>
2
3 int main() {
4
std::cout << "Hello World";
5 }
1 Hello World
You might notice that we prepend cout with a std::. C++ also introduces a concept called namespaces. Namespaces reduce the
possibility of name collision. This is so that when we use several
libraries, or write our own functions, we can differentiate possible
identifiers of the same name.
This also means we have to write std::cout and std::cin for every
mention of cout or cin or any other identifier in the std namespace.
We can use the keyword using to sort of set a “default” namespace.
1 using namespace std;
2
3 int main() {
4
cout << "Hello World"; // no need for std:: anymore
5 }
1 Hello World
69
The << is actually an operator called the stream insertion operator.
Notice the angle brackets point towards cout which is known as the
“character output stream”. Here we insert the text "Hello World"
into the character output stream.
Conversely, there is a >> operator called the stream extraction operator. We can, you guessed it, obtain values from the keyboard as
well. We extract from cin, known as the “character input stream”.
1 int main() {
2
int x;
3
cout << "Enter an integer: ";
4
cin >> x;
5
cout << x * x;
6 }
1 Enter an integer: 9
2 81
In the same way that printf and scanf can handle multiple in/outputs, we can do that with cout and cin as well.
1 int x;
2 float y;
3 cin >> x >> y;
4 cout << y << " " << x;
1 25 1001.2
2 1001.2 25
We don’t have to define a format anymore as C++ will automatically
try to cast inputs and outputs to their associated types.
Booleans
C++ introduces the bool type to represent true and false. While in C
we already had a sort of boolean capability with just 1 representing
true and 0 representing false, C++ makes it an official type.
1 bool a = true;
2 if (a) {
3
bool flag = false;
70
4 }
Strings
In C++, instead of char[]s as strings, strings are now a type in and
of itself.
1 int main() {
2
string s = "Hello World";
3 }
This affords us some useful properties, like knowing its length.
1 string s = "Test";
2 s.length() == 4; // true
Here s.length() is actually invoking a member function, which is
possible thanks to classes. We’ll talk about classes later.
Another useful functionality that is afforded by the C++ string type
is concatenation. We can join strings together easily which we can’t
do as easily in C.
1 string text = "Good ";
2 if (isMorning()) {
3
text += "morning!";
4 } else if (isAfternoon()) {
5
text += "afternoon!";
6 } else {
7
text += "evening!";
8 }
9
10 cout << text;
1 Good morning!
As another example, we can use concatenation to wrap a string in
parentheses.
1 void parenthesize(string* s) {
2
*s = "(" + *s + ")";
3 }
71
4
5 int main() {
6
string a = "Hello?";
7
parenthesize(&a);
8
cout << a;
9 }
1 (Hello?)
References
No, this isn’t the part where I link to websites for references. Rather,
this is a new variable type almost similar to pointers.
1 int x = 10;
2 int& xRef = x;
Again, unfortunately it uses an existing symbol. The & does not mean
the address operator, but rather to indicate that this is a reference.
References create aliases to the value referenced. That is, a reference simply gives an existing value a new name.
If we take the address of both a value and its reference, we get the
same address.
1 &x == &xRef; // true
And thus, in our example, any change to x is reflected in xRef and
vice versa.
1 x = 100;
2 x++;
3 xRef == 101; // true
4 xRef--;
5 x == 100; // true
Both these values are effectively the same thing!
References are also able to be passed to functions.
1 void addOne(int& x) {
2
x++;
72
3 }
4
5 int main() {
6
int a = 100;
7
addOne(a);
8
a == 101; // true
9 }
No need for pointers and dereferencing and so on any more!
Memory Allocation
Dynamic memory allocation in C++ is different too.
Instead of
malloc and free, we use new and delete.
1 int main() {
2
int* x = new int(5);
3
*x == 5; // true
4 }
No need to manually perform malloc and sizeof, C++ does it all for
you concisely. We can even initialize the value right away.
To free memory, we simply do a delete
1 delete x;
To allocate for an array is simple as well.
1 int *arr = new int[100]; // allocate 100 ints
And to free an array,
1 delete[] arr;
As you can see, C++ makes it easy and we don’t have to fiddle with
measuring the size of objects when we allocate.
73
Classes
Ah, we’ve put this aside for too long now. Introducing: classes. First,
we’ll demonstrate the card randomizing program from before but
with the use of classes. We’ll break this down piece by piece later.
1 #include<iostream>
2 #include<cstdlib>
3 #include<ctime>
4
5 using namespace std;
6
7 class Card {
8
9 private:
10
char value;
11
char suit;
12
char randomizeValue() {
13
char value[] = {'A', '2', '3', '4', '5', '6',
14
'7', '8', '9', 'T', 'J', 'Q', 'K'};
15
16
return value[rand() % 13];
}
17
18
char randomizeSuit() {
19
char suits[] = {'H', 'D', 'S', 'C'};
20
return suits[rand() % 4];
21
}
22
23 public:
24
Card() {
25
26
this->randomize();
}
27
28
Card(char value, char suit) {
29
this->value = value;
30
this->suit = suit;
31
}
32
74
33
char getValue() {
34
return this->value;
35
}
36
37
char getSuit() {
38
return this->suit;
39
}
40
41
void setValue(char value) {
42
this->value = value;
43
}
44
45
void setSuit(char suit) {
46
this->suit = suit;
47
}
48
49
string toString() {
50
char st[2] = {this->value, this->suit};
51
return string(st);
52
}
53
54
void randomize() {
55
this->value = this->randomizeValue();
56
this->suit = this->randomizeSuit();
57
}
58 };
59
60 int main() {
61
Card c;
62
cout << c.toString() << endl;
63 }
1 TD
Let’s break this down.
1 #include<iostream>
2 #include<cstdlib>
3 #include<ctime>
75
4
5 using namespace std;
We include the iostream header for cout. cstdlib and ctime includes C’s stdlib.h and time.h headers. We also set the default
namespace to std.
1 class Card {
2
. . .
3 };
This declares a new class called Card. When we name classes we
name them in title-case by convention. For example, if we were
to make a class representing a sports car, we’d name the class
SportsCar.
So what are classes?
Classes are a template or a blueprint for creating objects. You see,
it should be noted that it is not the class the holds the data or information, but rather it is the objects that are based from the class.
Objects are instances of a class; that is, objects hold the data.
Just like C structs (C++ also has structs, by the way), we sort of
create a template of variables that can go into a structure (the class).
But we have to declare a variable of that type to start using the structure (the object).
1 struct Card { // the "class"
2
char value;
3
char suit;
4 };
5 typedef struct Card Card;
6
7 int main() {
8
Card c; // c is the "object"
9 }
Which is equivalent to:
1 class Card {
2
char value;
76
3
char suit;
4 };
5
6 int main() {
7
Card c;
8 }
The variables value and suit here are thus called data members.
To instantiate a class, a constructor is called to initialize its data
members. By default, this constructor is blank but we can override
it to set some more sane defaults. The constructor is defined like a
function with the same name as the class.
1 class Card {
2
char value;
3
char suit;
4
5
Card() {
6
this->value = 'A';
7
this->suit = 'H';
8
}
9 };
The constructor is run whenever we construct or create a new object
from the class:
1 Card c; // runs the constructor
2 c.value == 'A'; // true
3 c.suit == 'H'; // true
You might wonder what this is. this is a special pointer to the
current object being created. This special pointer is implicitly passed
like an argument to the constructor like so:
1 Card(Card* this) {
2
this->value = 'A';
3
this->suit = 'H';
4 }
However, it is actually legal to simply write the following as your constructor:
77
1 Card() {
2
value = 'A';
3
suit = 'H';
4 }
As there is no ambiguity as to which value or suit you mean except
for its own data member. When there is ambiguity, that’s when you
have to use this.
It’s still a good idea, however, to use this either way.
A constructor may accept no arguments, like seen above, or it may
accept any number of arguments within the parentheses.
We can declare multiple constructors with differing signatures or arguments accepted. The constructor that matches the signature or
arguments will be the one that executes.
1 class Card {
2
char value;
3
char suit;
4
5
Card() {
6
this->value = 'A';
7
this->suit = 'H';
8
}
9
10
Card(char value, char suit) {
11
this->value = value;
12
this->suit = suit;
13
}
14 }
15
16 int main() {
17
Card c;
18
c.value == 'A'; // true
19
c.suit == 'H'; // true
20
Card d('T', 'D');
21
d.value == 'T'; // true
22
d.suit == 'D'; // true
78
23 }
The constructor with signature Card(char value, char
suit) is where ambiguity can happen, the use of this here
is required.
When an object is created, a constructor is run. On the flip side,
when an object is destroyed, a destructor is run.
Destructors are declared with a ~ followed by the name of the constructor.
1 class Card {
2
char value;
3
char suit;
4
5
Card() {
6
this->value = 'A';
7
this->suit = 'H';
8
}
9
10
Card(char value, char suit) {
11
this->value = value;
12
this->suit = suit;
13
}
14
15
~Card() { // the destructor
16
// good time to free allocated memory
17
}
18 }
The destructor is a good place to free any memory we might’ve allocated throughout the object’s life.
So aside from constructors, classes are like structures, but better?
How?
Instead of only being able to compose structures out of variables,
classes can also be composed of functions.
1 class Card {
79
2
char value;
3
char suit;
4
5
char randomizeValue() {
6
char value[] = {'A', '2', '3', '4', '5', '6',
7
'7', '8', '9', 'T', 'J', 'Q', 'K'};
8
return value[rand() % 13];
9
}
10
11
char randomizeSuit() {
12
char suits[] = {'H', 'D', 'S', 'C'};
13
return suits[rand() % 4];
14
}
15
16
void randomize() {
17
this->value = this->randomizeValue();
18
this->suit = this->randomizeSuit();
19
}
20 };
21
22 int main() {
23
Card c;
24
cout << c.toString() << endl;
25
c.randomize(); // randomizes card
26
cout << c.toString() << endl;
27 }
1 8H
2 2D
endl is part of the iostream header and is used as a platform-
specific end line. It’s a good idea to end lines with this instead
of '\n'.
Functions that are part of classes are thus called member functions.
Member functions also have a special pointer to the object itself,
80
this.
A call to c.randomize(), for example, acts like:
1 Card c;
2 randomize(&c);
With the function defined as:
1 void randomize(Card* this) {
2
this->value = this->randomizeValue();
3
this->suit = this->randomizeSuit();
4 }
Suppose the class Card has a member function setValue(char)
that sets the value of the card.
1 class Card {
2
3
. . .
4
5
void setValue(char value) {
6
this->value = value;
7
}
8 }
A call to c.setValue('2') would effectively be:
1 Card c;
2 setValue(&c, '2');
with setValue defined as:
1 setValue(Card* this, char value) {
2
this->value = value;
3 }
Another benefit to classes is the ability to hide member data and
functions via access specifiers.
Sometimes there are data members or functions that you don’t want
anybody else to access except for the object itself.
Why?
81
For simplicity and safety.
By hiding data and functions from other objects we decrease the
chance that other objects misuse and mistakenly modify data,
putting the object in an invalid state.
For example, we have a class called Account that stores the balance
of a client.
1 class Account {
2
string name;
3
float balance;
4 };
Suppose that in our program, balances can’t be negative. So we
have a function that handles crediting from our balance, making sure
the balance is never negative.
1 class Account {
2
string name;
3
float balance;
4
5
bool credit(float amount) {
6
if (balance - amount < 0) {
7
return false;
8
}
9
balance = balance - amount;
10
return true;
11
}
12 }
Accounting term: to credit is to decrease an asset. Conversely,
debit is to increase an asset.
In this case, what’s stopping others from directly accessing and modifying balance?
1 Account a;
2 a.balance = -100; // a is now an invalid account
In a larger program it’s almost too easy to make this simple mistake
and forget that balances can’t be negative. So to safeguard against
82
this mistake we can hide balance from modification outside of the
object.
This is what access specifiers do. There are 3 access specifiers:
1. private: this is the default access specifier. Strictly only member functions of the object can access these.
2. public: members with this access specifier is accessible from
any part of the program.
3. protected: similar to private, but derived classes can also
access these. We’ll touch on derived classes soon.
Sometimes variables or functions only make sense inside the context of the object, these would be good candidates for the private
access specifier.
Access specifiers are declared in the class like so:
1 class Account {
2
private:
3
string name;
4
float balance;
5
public:
6
bool credit(float amount) {
7
if (balance - amount < 0) {
8
return false;
9
}
10
balance = balance - amount;
11
return true;
12
}
13 };
Once we declare an access specifier, everything under it has that
type of access until we list a different one.
Although private is the default specifier it’s still a good idea to explicitly write down the private specifier.
For our Account example, now that its data members are private,
shouldn’t we be able to modify or at least retreive its value?
Well, right now we can’t. But there is a mechanism that we can im83
plement: mutators and accessors.
Mutators are public member functions that mutate or change a data
member’s value. They act as an interface to the value that is encapsulated or hidden inside an object.
Accessors, on the other hand, are also public member functions but
they merely access the encapsulated value. Allowing us a read-only
access to the value.
The credit(float) function is a great example of a mutator. It enables the valid mutation of the private balance value.
We can design an accessor that retrieves the balance of the
Account like so:
1 class Account {
2
private:
3
string name;
4
float balance;
5
public:
6
bool credit(float amount) {
7
if (balance - amount < 0) {
8
return false;
9
}
10
balance = balance - amount;
11
return true;
12
}
13
14
float getBalance() {
15
return balance;
16
}
17 };
It is often by convention that we name accessors as get followed by
the name of the value that is hidden (ex. getBalance for balance,
getName for name).
Conversely, by convention, we name mutators as set followed by
the name of the value (ex. setBalance for balance, setName for
name).
84
As for the Card class,
1 class Card {
2
private:
3
char value;
4
char suit;
5
. . .
6
public:
7
. . .
8
char getValue() {
9
return this->value;
10
}
11
12
char getSuit() {
13
return this->suit;
14
}
15
16
void setValue(char value) {
17
this->value = value;
18
}
19
20
void setSuit(char suit) {
21
this->suit = suit;
22
}
23 }
Gets are accessors, sets are mutators.
Templates
Remember our list that we implemented in C? And how it only handles ints? That’s right, C++ has the solution: templates.
We’ll implement our vector in C++ and at the same time look at how
to use templates.
1 template <class T>
2 class Vector {
3
4
private:
T* arr;
85
5
int size;
6
int capacity;
7
8
void resize() {
9
int newCapacity = 2 * size;
10
T* newArr = new T[capacity];
11
for (int i = 0; i < size; i++) {
12
newArr[i] = arr[i];
13
}
14
capacity = newCapacity;
15
delete[] arr;
16
arr = newArr;
17
}
18
19
20
public:
Vector() { // our initList()
21
size = 0;
22
capacity = 10;
23
arr = new T[capacity];
24
}
25
26
~Vector() {
27
28
delete[] arr;
}
29
30
31
void insertItem(int index, T item) {
if (index >= 0 && index <= size) {
32
if (size == capacity) {
33
resize();
34
}
35
for (int i = size; i > index; i--) {
36
arr[i] = arr[i - 1];
37
}
38
arr[index] = item;
39
size++;
40
}
41
return;
86
42
}
43
44
T& removeItem(int index) {
45
if (index >= 0 && index <= size - 1) {
46
if (capacity / 3 < size - 1) {
47
resize(list);
48
}
49
for (int i = index; i < size - 1; i++)
{
50
arr[i] = arr[i + 1];
51
}
52
size--;
53
}
54
}
55 };
Using the template keyword, we can have a placeholder so that our
class is more adaptable. In this case, we have a placeholder, T, that
should be a class.
We can use this template like so:
1 int main() {
2
Vector<string> stringVector;
3
Vector<int> intVector;
4 }
Here we declare stringVector a type of Vector<string>. Here
we pass string to our class template making every T in the class
definition equivalent to string. Thus by declaring Vector<string>,
the class definition becomes:
1 class Vector {
2
private:
3
string* arr; // T subbed for string!
4
int size;
5
int capacity;
6
7
8
public:
Vector() {
87
9
size = 0;
10
capacity = 10;
11
arr = new string[capacity];
12
}
13
14
~Vector() {
15
delete[] arr;
16
}
17
18
. . .
19 }
And it’s ints for intVector.
You might have noticed that we left out the getItem and setItem
functions. This is because we’ll introduce 1 more topic before implementing those, and that topic is: exception handling.
Exceptions
1 T& getItem(int index) {
2
if (index >= 0 && index <= size - 1) {
3
return arr[index];
4
}
5
throw "Invalid index";
6 }
7
8 void setItem(int index, T item) {
9
if (index >= 0 && index <= size - 1) {
10
arr[index] = item;
11
}
12
throw "Invalid index";
13 }
Exceptions allow us to bail from a function when we are unable to
give an expected result. Like in the case of passing a wrong index
to getItem.
A call to getItem with an invalid index has no reasonable value and
thus we should instead throw an exception.
88
To throw an exception we write throw followed a value to throw.
The value we throw can be any value, but most commonly we use
an exception type.
If we don’t handle the exception by catching it, the program crashes.
To catch an exception, we use a try...catch block:
1 int main() {
2
Vector<int> vector;
3
try {
4
vector.getItem(5); // invalid index
5
} catch (string& message) {
6
cout << message << endl;
7
}
8 }
1 Invalid index
Since the exception we threw was a string, we catch a string.
Optionally, we can catch for any exception by writing down an ellipsis
inside the catch parentheses. This is at the cost of not being able
to know what exception was thrown.
1 int main(){
2
Vector<int> vector;
3
try {
4
vector.getItem(5);
5
} catch (...) {
6
cout << "An error was thrown! Dunno what but
something was thrown!" << endl;
7
}
8 }
1 An error was thrown! Dunno what but something was
thrown!
We can chain catches as well, so the appropriate exception is called:
1 int main() {
2
Vector<int> vector;
3
try {
89
4
vector.getItem(2);
5
} catch (int exc) {
6
cout << "An int exception: " << exc << endl;
7
} catch (string message) {
8
cout << message << endl;
9
} catch (...) {
10
cout << "anything goes" << endl;
11
}
12 }
C++ provides a list of standard exceptions defined in the stdexcept
header.
1 #include<stdexcept>
For
reference:
https://www.tutorialspoint.com/cplusplus/cpp_
exceptions_handling.htm
Looking through the description of the exceptions, the out_of_range
exception looks like an appropriate exception for us to use! It’s an
exception meant for operations that fall out of range.
We can learn how to use this by checking documentation: https://
www.cplusplus.com/reference/stdexcept/out_of_range/
We can see that it accepts a string, what_arg, which is a string to
explain what went wrong. Great, let’s use this:
1 template <class T>
2 class Vector {
3
. . .
4
5
T& getItem(int index) {
6
if (index >= 0 && index <= size - 1) {
7
return arr[index];
8
}
9
throw out_of_range("Invalid index!");
10
}
11 };
And to catch it:
90
1 int main() {
2
Vector<int> vector;
3
try {
4
vector.getItem(2);
5
} catch (out_of_range& exception) {
6
cout << "out_of_range exception: " <<
exception.what() << endl;
7
}
8 }
1 out_of_range exception: Invalid index!
We can create our own exception to fit our use case:
1 struct VectorExpression : public exception {
2
const char* what() const throw() {
3
return "Vector exception";
4
}
5 };
6
7 template <class T>
8 class Vector {
9
public:
10
. . .
11
12
T& getItem(int index) {
13
if (index >= 0 && index <= size - 1) {
14
return arr[index];
15
}
16
throw VectorExpression();
17
}
18 };
And to catch:
1 int main() {
2
Vector<int> vector;
3
try {
4
5
vector.getItem(5);
} catch (VectorExpression& e) {
91
6
cout << e.what() << endl;
7
} catch (exception& e) {
8
// other exceptions
9
}
10 }
1 Vector exception
So basically to create our own simple exception, we follow this template:
1 struct <exception name> : public exception {
2
const char* what() const throw() {
3
return <message>;
4
}
5 }
Where “exception name” is the name you want to give our new
exception and “message” is the message we get when we do
exception.what().
We’ll touch on what the colon means later.
92
Code Reuse
In the same way that we create functions to be able to reuse functionality, we are able to reuse whole classes.
There are many different approaches to reusing code with classes,
let’s first talk about the first approach: composition.
Composition
Let’s continue our Card class example.
1 class Card {
2
private:
3
char value;
4
char suit;
5
6
. . . // see previous Card class
7 };
This already is an example of composition. We make use of existing
data types or structures, in this case a char, to form a Card. In a
sense, we are reusing the fact that chars hold character data.
For a more clear example, let’s create a class called a Deck. A Deck
will represent a deck of cards.
1 class Deck {
2
3 };
What should we put in here?
That’s right, we can compose our Card class in.
1 class Deck {
2
private:
3
Card cards[52];
4 };
Just like that, we don’t have to reimplement how to represent or display these cards; as the Card class and, by extension, objects already have that implemented.
93
peek() takes a peek at the top card of the deck.
1 class Deck {
2
private:
3
Card cards[52];
4
5
public:
6
Deck() {
7
for (int i = 0; i < 52; i++) {
8
cards[i] = Card();
9
}
10
}
11
12
Card& peek() {
13
return cards[51];
14
}
15
16
void toString() {
17
for (int i = 0; i < 52; i++) {
18
cout << cards[i].toString() << '\n';
19
}
20
}
21 };
22
23 int main() {
24
Deck d;
25
cout << d.peek().toString() << endl;
26 }
1 9H
Friendship
Note that we are not able to access the private members of Card
from Deck, just as designed.
If we wanted to, however, there is a way for objects from Deck to
access all private members of Card. And that is to designate Deck
as a friend to Card.
94
1 class Card {
2
private:
3
char value;
4
char suit;
5
6
friend class Deck;
7 };
Who else better to give access to your private members but to your
friends?
Declaring class Deck a friend of Card allows all functions of Deck
access to Card’s private members.
On the other hand, if we only wanted to allow some functions of Deck
access to Card’s private members we can declare only a function of
Deck as a friend like so:
1 class Card {
2
private:
3
char value;
4
char suit;
5
6
friend Card& Deck::peek();
7 };
This allows only the peek function of Deck access to all private members of Card. All other functions of Deck won’t have access to Card’s
private members.
Ergo, only in peek() can we access value or suit:
1 Card& Deck::peek() {
2
this->cards[51].value; // valid
3
return this->cards[51];
4 }
5
6 string Deck::toString() {
7
this->cards[51].value; // INVALID!
8
return "";
9 }
95
Take note of the following points about friendship:
• Friendship should be used sparingly.
Declaring too many
friendships lessens the benefit and value of declaring the
member private in the first place.
• Friendship is not mutual. Declaring class A a friend of class B
does not make class B a friend of A.
• Friendship is not inherited.
Speaking of inheritance…
Inheritance
Inheritance is another one of the many methods to achieve code
reuse.
Most of the time, you might catch yourself writing classes that act
similar, but not quite, to other classes. For example, our Card class
right now represents playing cards. Maybe we want to represent
other cards as well? Like UNO perhaps or Monopoly Deal?
With inheritance we can specify that a class should inherit the members of an existing class. We call this existing class the base class,
and the new class the derived class.
In contrast to composition, which has a has-a relationship, inheritance has an is-a relationship.
That is, when we say that an UnoCard class, for example, inherits
from Card, we are saying that UnoCard is a Card.
In an is-a relationship, the derived class can be treated as an object
of its base class. That is, we can treat an UnoCard as a Card in code.
To declare a class a derived class we add a : after the class name,
followed by an access specifier and the base class to inherit from.
Like so:
1 class Card { // base class
2
protected:
3
char value;
4
char suit;
96
5 };
6
7 class UnoCard : public Card { // derived class
8
public:
9
string toString() {
10
// treat suit as UNO colors instead
11
}
12 };
The same access specifiers are available here, and we can choose:
• public, to inherit all public and protected members of the
base class. They keep their access specifiers from the base
class in the derived class.
• protected, to inherit all public and protected members of
the base class. They become protected members of the derived class (including constructors).
• private, to inherit all public and protected members of the
base class. They become private members of the derived
class (including constructors).
However, most often you’ll just be using the public specifier.
To differentiate between public and protected or private, an example:
1 class Pen {
2
3
4
5
private:
int inkLevel;
protected:
void preparePen() {
6
7
8
9
// prepare pen for writing
}
public:
Pen() {
10
inkLevel = 50;
11
preparePen();
12
}
13
void write(string text) {
97
14
// function to write text as a pen
15
}
16 };
17
18 class BallpointPen : public Pen {
19 };
20
21 class GelPen : protected Pen {
22 };
23
24 class FountainPen : private Pen {
25 };
Using the public access specifier will allow for an is-a relationship.
Using protected would mean the class has an is-a relationship only
for itself and its derived classes. private means the class is “implemented in terms of” the base class.
To be honest, I have a hard time looking for cases where we won’t
use public instead. If you want, you can read more about when
to use non-public inheritance here: https://stackoverflow.com/q/
7952375 and https://stackoverflow.com/q/656224
Static Members
A static member of function of a class is a member that is shared
by all objects of the class. That is, no matter how many objects are
created from the class, there only exists 1 copy of the static member.
We can declare a static member with the static keyword.
1 class Block {
2
public:
3
static int COUNT;
4
5
Block() {
6
}
7 };
And we have to initialize a static member outside of any function
98
while using the scope resolution operator, :::
1 int Block::COUNT = 0; // initialize COUNT that belongs
to class Block
2
3 int main() {
4
Block::COUNT == 0; // true!
5 }
The benefit here is that all objects of Block share this value. If we
have to keep track of the amount of Blocks for example, this is a
great solution.
1 class Block {
2
public:
3
static int COUNT;
4
5
Block() {
6
COUNT++;
7
}
8 };
9
10 int Block::COUNT = 0;
11
12 int main() {
13
Block a;
14
Block b;
15
16
Block::COUNT == 2; // true!
17
&a.COUNT == &b.COUNT; // true! they're one and the
same value
18 }
On the other hand, if COUNT wasn’t static, COUNT wouldn’t be accurate since separate objects would have their own version of COUNT.
1 class Block {
2
3
public:
int COUNT = 0;
4
99
5
Block () {
6
COUNT++;
7
}
8 };
9
10 int main() {
11
Block a; // a.COUNT = 1;
12
Block b; // b.COUNT = 1;
13
&a.COUNT == &b.COUNT; // false!
14 }
Beside static data members, static member functions exist.
1 class Block {
2
private:
3
static int COUNT;
4
public:
5
static int getCount() {
6
return COUNT;
7
}
8 };
9
10 int Block::COUNT = 0;
11
12 int main() {
13
Block a;
14
Block::getCount() == 1; // true!
15 }
Static functions are not able to access the this pointer and thus are
unable to access non-static members of the class.
A common use of static functions is to group general utility functions under one class. Instead of sprawling general utility functions
everywhere, we can put them under a “utility” class, like so:
1 class Math {
2
3
4
public:
static int square(int a) {
return a * a;
100
5
}
6
7
static int max(int a, int b) {
8
// do max between a and b
9
}
10
11
// other utility functions
12 };
13
14 int main() {
15
Math::square(3) == 9; // true!
16 }
Function and Operator Overloading and Overriding
You might have already seen this, back when we were discussing
about constructors and how there can be multiple of them. That
was a form of overloading.
Function overloading is where we use the same identifier, but perform different behaviors.
Throw back to our constructors:
1 class Block {
2
private:
3
string type;
4
public:
5
Block() {
6
type = "dirt";
7
}
8
9
Block(string type) {
10
this->type = type;
11
}
12 };
The above is already a form of overloading; we are overloading the
identifier Block to perform different behaviors.
101
Another example:
1 void print(int i) {
2
cout << "Integer: " << i << endl;
3 }
4
5 void print(double d) {
6
cout << "Double: " << d << endl;
7 }
8
9 void print(char c) {
10
cout << "Char: " << c << endl;
11 }
12
13 int main() {
14
print(1); // Integer: 1
15
print(1.2); // Double: 1.2
16
print('a'); // Char: a
17 }
The correct function is run with the matching argument.
Surprise, operators can be overloaded as well. Suppose we are to
compare 2 Blocks and see if they’re the same type. We have to overload the == operator; otherwise C++ doesn’t know how to compare
2 Blocks with ==.
We call this overloading and not overriding because we’re building on top of the operator and not overwriting the operator’s
functionality with other types.
1 int main() {
2
Block a;
3
Block b;
4
// a == b; // not possible, C++ doesn't recognize
comparing 2 Blocks
5 }
1 class Block {
2
private:
102
3
string type;
4
5
public:
6
Block(string type){
7
this->type = type;
8
}
9
10
bool operator==(Block& b) {
11
return this->type == b.type;
12
}
13 };
14
15 int main() {
16
Block a("grass");
17
Block b("grass");
18
19
a == b; // true!
20
21
Block c("dirt");
22
23
a == c; // false!
24 }
We can also overload the other operators like +, -, =, +=, <, etc.
Function overriding is overwriting existing behavior with a new one.
In the context of inheritance, this allows us to implement a different
behavior in derived classes.
Consider our Block class:
1 class Block {
2
private:
3
int x;
4
int y;
5
int z;
6
7
8
9
public:
int getX() {
return x;
103
10
}
11
12
int getY() {
13
return y;
14
}
15
16
int getZ() {
17
return z;
18
}
19 };
Let’s say x, y, and z are the coordinates of the Minecraft block in the
world.
Minecraft blocks can be placed and destroyed:
1 class Block {
2
. . .
3
public:
4
. . .
5
void place(int x, int y, int z) {
6
// place block at coordinate
7
this->x = x;
8
this->y = y;
9
this->z = z;
10
}
11
12
void destroy(string tool) {
13
// destroy block
14
cout << "Destroyed block in 2 seconds\n";
15
}
16 };
This is a generic block that can be destroyed by any tool in 2 second.
Now, consider that dirt, sand, and gravel blocks are destroyed faster
with a shovel. So we want a different destroy function as a dirt,
sand, or gravel block.
1 class DirtBlock : public Block { // DirtBlock is-a
Block
104
2
public:
3
void destroy(string tool) { // override base
class Block::destroy
4
// destroy block
5
if (tool == "shovel") {
6
cout << "Destroyed block in half a
second\n";
7
} else {
8
Block::destroy(tool); // use base
class' destroy function
9
}
10
}
11 }
1 int main() {
2
Block generic;
3
DirtBlock dirtA;
4
DirtBlock dirtB;
5
generic.destroy("shovel");
6
dirtA.destroy("shovel");
7
dirtB.destroy("hand");
8 }
1 Destroyed block in 2 seconds
2 Destroyed block in half a second
3 Destroyed block in 2 seconds
Note that, in overriding, the signatures of the functions are the same.
And from the overridden function, we can still use the original function (as seen in line 8 of DirtBlock declaration).
Basically, if signatures are different (accept different arguments), it
is an overload. If signatures are the same, it is an override.
105
Polymorphism
Overriding flows quite well into the topic of polymorphism. “Poly” for
many, “morph” for form: many forms.
Consider our Block example again.
1 class Block {
2
public:
3
void destroy(string tool) {
4
cout << "Generic block destroy with " <<
tool << "\n";
5
}
6 };
We can call on the destroy function to perform a generic block destroy.
1 int main() {
2
Block b;
3
b.destroy("shovel");
4 }
1 Generic block destroy with shovel
Now suppose we create a function to destroy an array of blocks.
1 void destroyBlocks(Block* bArr, int n, string tool) {
2
for (int i = 0; i < n; i++) {
3
4
bArr[i].destroy(tool);
}
5 }
1 int main() {
2
Block blocks[5];
3
destroyBlocks(blocks, 5, "pickaxe");
4 }
1 Generic block destroy with pickaxe
2 Generic block destroy with pickaxe
3 Generic block destroy with pickaxe
4 Generic block destroy with pickaxe
5 Generic block destroy with pickaxe
106
destroyBlocks will accept an array of Blocks, size of the array, and
the tool to destroy the Blocks with.
Now what if we wanted to destroy an array of DirtBlocks too?
1 class DirtBlock : public Block {
2
public:
3
void destroy(string tool) { // overridden
destroy
4
if (tool == "shovel") {
5
cout << "Faster dirt block destroy
with shovel\n";
6
} else {
7
Block::destroy(tool); // generic
destroy
8
}
9
}
10 };
DirtBlocks have their destroy function overridden to allow for
faster block destroy with a shovel.
Now, wouldn’t it be nice if we can use the same destroyBlocks(Block*,
int, string) function above? After all, with polymorphism, both
these classes are Blocks! And that’s right, we can actually use the
same function!
C++ won’t complain that you’re passing a DirtBlock when it expects
a Block because a DirtBlock is a Block! Thanks, polymorphism!
1 int main() {
2
DirtBlock dirtBlock[5];
3
destroyBlocks(dirtBlock, 5, "shovel");
4 }
1 Generic block destroy with shovel
2 Generic block destroy with shovel
3 Generic block destroy with shovel
4 Generic block destroy with shovel
5 Generic block destroy with shovel
107
But wait, we’re not getting the right output! That’s because from
the perspective of the compiler only the Block‘s version of destroy
exists. It doesn’t know about any other derived classes’ version like
DirtBlock’s.
It doesn’t know until we tell it that it should expect a different version.
We can tell the compiler via the virtual keyword.
1 class Block {
2
public;
3
virtual void destroy(string tool) {
4
cout << "Generic block destroy with " <<
tool << "\n";
5
}
6 };
By declaring destroy virtual, we are telling the compiler not to take
Block‘s destroy function by default but to look and use for the de-
rived class’ function (if it exists).
With that, a call to destroyBlocks will yield the correct output this
time.
1 int main() {
2
DirtBlock dirtBlock[5];
3
destroyBlocks(dirtBlock, 5, "shovel");
4 }
1 Faster dirt block destroy with shovel
2 Faster dirt block destroy with shovel
3 Faster dirt block destroy with shovel
4 Faster dirt block destroy with shovel
5 Faster dirt block destroy with shovel
Here’s another demonstration of polymorphism using pointers.
1 int main() {
2
Block* blockPtr;
3
Block generic;
4
DirtBlock dirtBlock;
5
6
blockPtr = &generic;
108
7
blockPtr->destroy("stick");
8
9
blockPtr = &dirtBlock;
10
blockPtr->destroy("shovel");
11 }
1 Generic block destroy with stick
2 Faster dirt block destroy with shovel
Suppose DirtBlock has a function called plant:
1 class DirtBlock : public Block {
2
public:
3
. . .
4
void plant(string seed) {
5
cout << "Plant " << seed << " seed\n";
6
}
7 };
In the context of using a Block*, that means we won’t be able to
access the plant function:
1 int main() {
2
DirtBlock dirtBlock;
3
Block* blockPtr;
4
5
blockPtr = &dirtBlock;
6
// blockPtr->plant("wheat"); // invalid!
7 }
That is because from the perspective of blockPtr, which is a
Block*, there is no plant function. The plant function only exists
in DirtBlock.
We can, however, cast blockPtr to a DirtBlock* so that we are
able to use the plant function.
1 ((DirtBlock*) blockPtr)->plant("wheat");
Be careful though, this will only work because we definitely know
that blockPtr holds a DirtBlock*. C++ won’t even warn you that
109
you’re trying to cast incompatible types and will crash if you’re not
careful.
Since we’re talking many forms, we also happen to be able to chain
inherit.
1 class Block {
2
public:
3
virtual void destroy() {
4
// block is recoverable after breaking
5
}
6 };
7
8 class FragileBlock : public Block {
9
public:
10
virtual void destroy() {
11
// block is lost after breaking
12
}
13 };
14
15 class GlassBlock : public FragileBlock {
16
// resulting destroy function here is
FragileBlock::destroy()
17 };
Here GlassBlock is both a Block and a FragileBlock.
This is also valid:
1 class GlassBlock : public Block, public FragileBlock {
2 };
Now suppose we make a Block that we’re sure no one wants to inherit from. Or we don’t want other Blocks to inherit from. We can
use the final keyword in C++.
1 class BedrockBlock final : public Block {
2
3 };
4
5 class MyBlock : public BedrockBlock { // invalid,
BedrockBlock is final!
110
6 };
final can also be used on functions, preventing the overriding of
the function. This could be useful to make sure that your definition
of the function is never changed down the line.
1 class Block {
2
public:
3
int getSize() final {
4
return 64;
5
}
6 }
7
8 class DirtBlock : public Block {
9
public:
10
int getSize() { // error! getSize is final in
Block
11
return 32;
12
}
13 }
Now that we’ve got a little taste of polymorphism, let’s go further.
Polymorphism paves the way for quite a useful mechanism: interfaces.
Interfaces
Let’s first talk about what interfaces are.
Take a look at your phone or your laptop, how are you interacting
with it? A touch screen? A keyboard? Those are your interfaces.
Now look at someone else’s phone or laptop, you probably know how
to use that as well.
Notice how as long as you know how to use a touch screen or use
a keyboard, you are able to use those devices. In the same way,
our code can have interfaces that allow other pieces of code to use
them. And as long as that code knows how to use the interface,
they’ll be able to use our code.
111
We can create interfaces by declaring an abstract class with pure
virtual functions.
An abstract class is a class that cannot be instantiated. It’s purpose
is to be a base class for others to inherit from.
The reason an abstract class cannot be instantiated is because its
definition is incomplete. That is, there exists some function in the
abstract class that is pure virtual.
One can inherit the abstract base class and implement or define the
pure virtual function by overriding it. The resulting class can thus be
instantiated. An instantiable class we call a concrete class.
A pure virtual function is a function in a class that has no definition.
Basically by declaring a function pure virtual, you are requiring for it
to be implemented before you can instantiate an object of the class.
The following code demonstrates abstract classes and pure virtual
functions:
1 class Touchscreen { // abstract class
2
public:
3
virtual void tap(int x, int y) = 0; // pure
virtual
4 }
5
6 class Keyboard { // abstract class
7
public:
8
virtual void press(char key) = 0; // pure
virtual
9 }
Since Touchscreen and Keyboard both have pure virtual functions
they are considered abstract.
It should be noted that we can have pre-implemented functions in
an abstract class:
1 class Mouse { // still an abstract class
2
3
public:
virtual void click(int x, int y) = 0; // pure
virtual
112
4
5
void doubleClick(int x, int y) { // double
click!
6
click(x, y);
7
click(x, y);
8
}
9 };
The benefit to using interfaces is that if you want some different
behavior, you can swap out the class for a different one—as long as
it implements the correct interfaces.
For instance, we know different brand headphones sound different
according to how they were physically implemented by the manufacturer. We can sort of model that here:
Let’s design our Headphone interface. With an headphone we can
“plug in” into a headphone jack, so let’s add that:
1 class Jack {
2
private:
3
Headphone& headphone;
4
Sound& sound;
5
6
7
public:
8
Jack(Sound& sound) {
9
this->sound = sound;
10
}
11
12
void plugIn(Headphone& headphone) {
13
this->headphone = headphone;
14
// try to start playing music through jack
15
headphone.stream(sound);
16
};
17 };
And a headphone can accept music with stream and play it back
with listen:
1 class Headphone {
113
2
public:
3
virtual void stream(Sound&) = 0;
4
virtual void listen() = 0;
5 };
Now our manufacturers can try to make the best headphone they
can and it’ll work with any Jack:
1 class SonyMXWhatevers : public Headphone {
2
3
public:
void stream(Sound& sound) {
4
5
// do Sony processing
}
6
7
void listen() {
8
9
// do Sony magic with the music
}
10 };
11
12 class SennheiserHD600 : public Headphone {
13
14
public:
void stream(Sound& sound) {
15
// accept the sound and add the
Sennheiser-sauce
16
}
17
18
void listen() {
19
20
// do your Sennheiser thing
}
21 };
22
23 class AudioTechnicaM50 : public Headphone {
24
25
public:
void stream(Sound& sound) {
26
27
// AT specific sound playing
}
28
29
void listen() {
114
30
// Audio-Technica sound signature
31
}
32 };
Then with all these headphones, we can plug any of them into a
Jack and Jack would know what to do with them. Then we can take
a listen and hear the different sounding headphones one by one:
1 int main() {
2
Sound someMusicStream;
3
Jack jack(someMusicStream);
4
5
SonyMXWhatevers sonys;
6
SennheiserHD600 hd600;
7
AudioTechnicaM50 atm50;
8
9
Headphone* headphones[3] = {&sonys, &hd600,
&atm50};
10
for (int i = 0; i < 3; i++) {
11
jack.plugIn(*headphones[i]);
12
headphones[i]->listen();
13
}
14 }
The call to listen will perform the manufacturer-specific one, and
any new manufacturer will only have to implement these functions
to be able to work as a “headphone”.
If you can, always separate out the interface from the implementation. You never know when you’re the new manufacturer looking to
make a new “headphone”.
115
Download