Fundamentals of Programming - Revision

A quick walkthrough on what-you-need-to-remember on previous course (CSC10012 - Fundamentals of Programming).

Table of Contents

1. Programming tools

In this course: g++ and any IDEs/editors that you feel comfortable with.

Conflicts between compilers

If you accidentally used Visual Studio in previous courses, you might notice some differences in how the code behaves in this course.

Example
#include<iostream>

using namespace std;

int main() {
    int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    int max_element = INT_MIN;

    for (int i = 0; i < 10; ++i) {
        if (a[i] > max_element) {
            max_element = a[i];
        }
    }

    cout << "The maximum element is " << max_element << "\n";

    return 0;
}

2. C++ Basics

Order of increment/decrement operators

Key points: If the operator stands before the variable, then we execute the operator first.

For example:

int a = 10, b = 10;

cout << a++ << " " << ++b;
// 10 11
// Note that both a and b now holds the value of 11.

Numerical Division

TL;DR:

  1. Do not divide by 0.
  2. Before dividing something, think-again on what-you-want-to-get.

Example:

int a = 5;
int b = 2;

cout << a / b; // ?

// Fix:
cout << (float)a / b;
cout << (a * 1.0 / b); // Easier

Variables scope

TL;DR: variables only exist in their scope.

#include<iostream>
using namespace std;

int main() {
    int x = 5;

    {
        int x = 10; // ??
        x = x * 2;
        cout << x << "\n";
    }
    
    cout << x << "\n";

    return 0;
}

Some notable scopes:

Example: global & local variable
#include<iostream>
using namespace std;

int global = 10;

int main() {
    int local = 5;

    {
        int local = 10;
        cout << local << " " << global << "\n";
        global = global + local;
    }
    
    cout << local << " " << global << "\n";

    return 0;
}

3. Functions

Basic usage

return_type function_name(datatype1 param1, [datatype2 param2, ..]) {
    // do something..

    return ..
}

Example:

// Adding two integers
int add(int a, int b) {
    return a + b;
}

Value-returning vs. void function

Differences? the first one must-returning-something, the other don't.

If your function has a return-value, please remember to return something (!)

// Adding two integers
int add(int a, int b) {
    int x = a + b;
}

// This function would trigger a warning with g++ compilers (Warning : No return statement in function returning non-void).

A void function is a function that does not return any value.

// Say hello to {name}
void hello(string name) {
    cout << "Hello, " << name << "!\n";
}

Function prototype vs. definition

TL;DR:

Example:

// Function prototype:
int add(int a, int b);
int add(int, int); // note that this also works!

// Function definition:
int add(int a, int b) {
    return a + b;
}

Parameters vs. arguments

int add(int a, int b) {
    return a + b;
}

// In this function, a and b are parameters.

add(5 + 6, 1); // (5 + 6) and 1 are arguments.

int a = 9, b = 10;
add(a, b); // a and b are arguments.

Pass by reference vs. pass by value

TL;DR:

Example:

// Pass by value
void increase_val(int a) {
    a = a + 1;
}

// Pass by reference
void increase_ref(int &a) {
    a = a + 1;
}

int a1 = 0;
increase_val(a1);

int a2 = 0;
increase_ref(a2);

cout << a1 << " " << a2;
// Output?

Important: with a pass-by-reference parameter, you can only pass a variable to it.

void increase(int &a) {
    a = a + 1;
}

// This works
int a = 10;
increase(a);

// This will NOT works
increase(10);

Default parameters

Parameters that have a default value assigned to them. If no value is provided when the function is called, the default value is used

// Adding three integers a, b and c
int add(int a, int b, int c = 0) {
    return a + b + c;
}

cout << sum(10, 11) << "\n"; // 21
cout << sum(10, 20, 30) << "\n"; // 60

Important: Default parameters must be declared on the right-hand side of any parameters without default values.

// This is valid
int add(int a, int b = 0, int c = 0) {
    return a + b + c;
}

// This is NOT valid
int add(int a = 0, int b, int c = 0) {
    return a + b + c;
}

Function overloading

TL;DR: same function name, but with different implementations.

int add(int a, int b) {
    return a + b;
}

// Valid overload-ers:
float add(float a, float b) {
    return a + b;
}

int add(int a, int b, int c) {
    return a + b + c;
}

// INVALID, why?
float add(int a, int b) {
    return a + b;
}

(Advanced) Lambda function

TL;DR: functions that goes against every coding convention short, inline functions for specific purpose, (most) cannot be reuse.

// Calculating sum of 2 integers a, b
int sumTwoNumber = [](int a, int b) {
    return a + b;
}

// Even shorter form:
auto sumTwoNumber = [](int a, int b) -> int {return a + b;};

cout << sumTwoNumber(10, 11) << "\n";

4. Structures

TL;DR: grouping variables to form a new datatype, yet meaningful.

Example:

struct Student {
    string ID;
    string name;
    double FP, calculus, DSA; // !!!
};

Set value for a Struct

You can either

Example:

// (with the previously defined Student structure)

Student An;
An.name = "An";
An.ID = "24121234";
An.FP = 10;
An.DSA = 10;
An.calculus = 10;

// or,
Student Binh{"24125678", "Binh", 9, 9, 9};

// Is the order in brace initialization important?

Deep & shallow copy

TL;DR: Be careful when working with structures that contain pointers.

If you use assignment operator on structs, C++ will create a shallow copy by default - which can create annoying behaviors.

struct Student {
    char* name; // To-be-continue ..
    double height, weight;
};

// Create a student named An
Student An;
An.name = new char[10];
strcpy(An.name, "An"); // #include<cstring> to use this.
An.weight = 75.5;
An.height = 150;

// Binh has the same height/weight of An, but with different name
Student Binh = An;
strcpy(Binh.name, "Binh");

cout << An.name << " " << Binh.name;
// What happened?
Explaination

In a shallow copy, Binh's name pointer points to the same memory location as An's name pointer. This means that if Binh changes the value pointed to by name, An's name will also change because they both refer to the same memory.

To fix the previous code:

Student Binh = An;
Binh.name = new char[10]; // Pointing the name pointer of Binh to a new address.
strcpy(Binh.name, "Binh");
// **Deep** copy - copy the value of the memory block, not only the address.

We will discuss this in more detail when we learn about pointers, or in later courses.

Size of a Struct

TL;DR: Structures with the same members but in different order might have different sizes in memory due to memory alignment requirements.

struct First {
    int a;
    double b;
    char c;
};

struct Second {
    int a;
    char c;
    double b;
};

cout << sizeof(First) << " " << sizeof(Second);
// 24 16

(Advanced) Operators overloading in Structs

TL;DR: operators are basically functions, therefore we can overloading them to define the ways operators works on structs.

Example
#include<iostream>

using namespace std;

struct Fraction {
    int numerator;
    int denominator;
};

// Comparions
bool operator<(Fraction a, Fraction b) {
    return a.numerator * b.denominator < b.numerator * a.denominator;
}

bool operator>(Fraction a, Fraction b) {
    return a.numerator * b.denominator > b.numerator * a.denominator;
}

bool operator==(Fraction a, Fraction b) {
    return a.numerator * b.denominator == b.numerator * a.denominator;
}

// Arithmetic
Fraction operator+(Fraction a, Fraction b) {
    Fraction result;

    // Naive method to add two fractions.
    result.numerator = a.numerator * b.denominator + b.numerator * a.denominator;
    result.denominator = a.denominator * b.denominator;

    // TODO: simplifying the result ..

    return result;
}

// Output
ostream& operator<<(ostream& out, Fraction a) {
    out << a.numerator << "/" << a.denominator;
    return out;
}

// Usage
int main() {
    Fraction a{1, 2};
    Fraction b{3, 4};

    // Compare
    if (a > b) {
        cout <<  "a > b";
    }
    else if (a < b) {
        cout << "a < b";
    }
    else {
        cout << "a = b";
    }

    cout << "\n";

    // Adding them up.
    cout << a + b << "\n";

    return 0;
}

(Advanced) Structured Binding (>= C++17)

TL;DR: quick accessing attributes of a structure.

// With previously-defined Student structure
Student Binh{"24125678", "Binh", 9, 9, 9};

auto [studentId, studentName, fpScore, calculusScore, dsaScore] = Binh;

cout << studentId << "\n"; // 24125678
cout << studentName << "\n"; // Binh
// ..

5. Arrays

TL;DR: group of same-datatype elements on continuous memory blocks.

1-D arrays

Quick initialization

// Create an array with 5 elements 1, 2, 3, 4, 5
int a[5] = {1, 2, 3, 4, 5};

// Create an array with 10 zeros.
int a[10] = {0};

int b[10] = {2}; // What could be b's values?

Note that if you don't set default values for elements in the array, the result may vary.

Indice range (i.e. valid indices)

Indices of an array of nn elements are from 0 to n1n - 1.

int a[5] = {1, 2, 3, 4, 5};
for (int i = 0; i < 10; ++i) {
    cout << a[i] << " ";
}
// What could be the output?

To randomly access any element: you can find that element directly using its position without needing to look at other elements first.

Passing 1-D arrays to function

1-D arrays, by default, are passed by reference to the function.

void mysteryFunction(int A[], int n) {
    for (int i = 0; i < n; ++i) {
        A[i] *= 2;
    }
}

int A[5] = {1, 2, 3, 4, 5};
mysteryFunction(A, 5);

for (int i = 0; i < 5; ++i) {
    cout << A[i] << " ";
}
// What will be the output?

2-D arrays

TL;DR: A matrix, or array of 1-D arrays.

Passing 2-D arrays to functions

You can omit the size of the first dimension of a 2D array in a function parameter.

// Printing a 2D array with less than / equals 20 rows/columns.

// Valid (prototypes)
void printMatrix(int A[][20], int n);
void printMatrix(int A[20][20], int n);

// Invalid!
void printMatrix(int A[][], int n);
void printMatrix(int A[20][], int n);

Just keep in mind, that the compiler needs to know the datatype of each element of any 1-D arrays.

nn-D arrays?

Just-keep-going.

Operations on Arrays

Inserting an element

Key points:

  1. Increase the capacity.
  2. Shift the array to the right starting from the position where you want to insert the new element.
  3. Insert the new element at the correct position.

Example:

// Task: Insert 7 to index i = 6;

int a[10] = {1, 2, 3, 4, 5, 6, 8, 9};
int n = 8; // Current size of the array.

int indexToInsert = 6;

// Step 1: increase the array's capacity (size).
n++;

// Step 2: shifting the array to the right to have a space for the new element
for (int i = n - 1; i > indexToInsert; --i) {
    a[i] = a[i - 1];
}

// Step 3: put the new element to its correct position.
a[indexToInsert] = 7;

// 1 2 3 4 5 6 7 8 9
for (int i = 0; i < n; ++i) {
    cout << a[i] << " ";
}

Removing an element

Same as Inserting, but shifting to the left and decrease the array's capacity.

6. Strings

Array of characters vs. String

Important: this section covers the definition of a string in C/C++. Do not confuse with std::string - which is a datatype in C++.

TL;DR: a string is an array of characters that ends with the null character \0.

// a is an array of characters.
char a[10] = {'h', 'e', 'l', 'l', 'o'};

// b is a string (C-string).
char b[10] = {'h', 'e', 'l', 'l', 'o', '\0'};

What is the following program's output?

#include<iostream>
#include<cstring> // to use strlen

using namespace std;

int main() {
    // a is an array of characters.
    char a[10] = {'h', 'e', 'l', 'l', 'o'};

    // b is a string (C-string).
    char b[10] = {'h', 'e', 'l', 'l', 'o', '\0'};

    cout << strlen(a) << " " << strlen(b);

    return 0;
}
Answer

Don't be surprised if you get the same result. The correct answer for questions above is undefined behavior - if you're luck, a's remaining elements will be 0 (naturally NULL).

7. Pointers

Pointers are variables that store the address of a memory location.

Important: Pointers themselves do not directly handle memory allocation (i.e., using new, delete, etc.).

Pointer Basics

int a = 10;

int *c = &a; // In human language: c is a pointer, holding the address of a.
*c = 11; // In human language: set a value for the memory block that c is pointing to.

cout << a; // What is the result?

Allocating and de-allocating memory blocks

int* a = new int{10};
cout << *a; // What is the output?

delete a; // Deallocate.

If you allocate memory for an array, the deallocation operator must use [].

int* a = new int[10]{1, 2, 3, 4, 5};

for (int i = 0; i < 10; ++i) {
    cout << a[i] << " ";
}

// What is the output?

delete[] a; // Deallocate.

Pointer-to-Pointer

A pointer is also a variable. Since it has a memory location, you can have a pointer that points to another pointer.

int a = 10;
int* c = &a;
int** d = &c;

**d = 11;

cout << a; // What is the result?

Pointer to Constant and Constant Pointers

TL;DR: difference between pointer to constant and constant pointer:

int a = 10, b = 11;

// Pointer to constant
const int* c = &a;
*c = 11; // You can't do this.
c = &b; // You can do this.

// Constant pointer
int* const c = &a;
*c = 11; // You can do this.
c = &b; // You can't do this.

Arrays and Pointers

Pointers to arrays hold the address of the first element in the array.

int* a = new int[10]{1, 2, 3, 4, 5};

cout << a << " " << &a[0]; // Should be the same.

Random access

Since arrays store elements in continuous memory locations, we can access any element directly using its position.

int* a = new int[10]{1, 2, 3, 4, 5};

// Element at index 1 i.e. 1 step from a[0]
cout << *(a + 1);

// Element at index 3 i.e. 3 steps from a[0]
cout << *(a + 3);

// Hence, a[i] has the same meaning as *(a + i)

Random access in 2D arrays

// a[i][j]
*(*(a + i) + j);

Structs and Pointers

With a pointer to a structure, you can use the -> (arrow) operator to access the structure's members.

// Using the definition of Student in Structure section.

Student* An = new Student;
An->name = "An";
An->DSA = 10;
// ..

// Note that these expressions are equivalent:
An->FP = 9.8;
(*An).FP = 9.8;

Using pointers in functions

Now you can returning an array via functions, using pointers.

// Initialize an array of n zeros;
int* initArray(int n) {
    int* a = new int[n]{0};
    return a;
}

(Advanced) Function Pointers

A function pointer stores the memory address of a function. This is possible because functions also reside in memory and have their own addresses.

Example
#include<iostream>

using namespace std;

bool ascending(int a, int b) {
    return a < b;
}

bool descending(int a, int b) {
    return a > b;
}

// Declaring the pointer to a function with two int parameters, returning a bool named `cmp`.
typedef bool(*cmp)(int, int);

// Interchange Sort, but the sorting condition is a parameter.
void sortArray(int* a, int n, cmp sortingCondition) {
    for (int i = 0; i < n - 1; ++i) {
        for (int j = i + 1; j < n; ++j) {
            // sortingCondition is a **function**
            if (!sortingCondition(a[i], a[j])) {
                swap(a[i], a[j]); // std::swap
            }
        }
    }
}

int main() {
    int* a = new int[10]{3, 2, 9, 1, 7, 5, 4, 6, 8, 10};
    int n = 10;

    sortArray(a, n, ascending);
    for (int i = 0; i < n; ++i) cout << a[i] << " ";
    cout << "\n";

    sortArray(a, n, descending);
    for (int i = 0; i < n; ++i) cout << a[i] << " ";
    cout << "\n";

    return 0;
}

// What is the result?

8. Linked Lists

TL;DR: Solving pain points of arrays in inserting/removing elements.

struct Node {
    int value;
    Node* pNext; // Pointer points to the next node.
};

Inserting a node

In 4 steps:

  1. Create a new node.
  2. Find the position where you want to insert the new node.
  3. Connect the previous node to the new node.
  4. Connect the new node to the next node.

Removing a node

In 3 steps:

  1. Find the node to be deleted.
  2. Connect the previous node to the node after the one being deleted.
  3. Delete the node.