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