Object-Oriented Programming in C++

Tips for navigating the slides:
  • Press O or Escape for overview mode.
  • Press the copy icon on the upper right of code blocks to copy the code

Class outline:

  • Classes
  • Pointers
  • Inheritance
  • Passing Arguments

Classes

Classes

C++ was created to add strong Object-Oriented Programming support for C.

Key differences from Python:

  • C++ uses the name of the class instead of __init__ for the constructor
  • C++ uses the keyword this instead of self
  • C++ uses the keyword void for the return type of a method when there is no return value
  • C++ has a semicolon ; after the brace } at the end of a class
    ⚠️ WARNING ⚠️ It is so easy to forget, but hard to catch
  • C++ uses keywords public and private for data hiding, or encapsulation
  • C++ defaults to everything being private. Python defaults to everything being public

Class Comparison


                    class Product:

                        def __init__(self, name, price, nutrition):
                            self.name = name
                            self.price = price
                            self.nutrition = nutrition
                            self.inventory = 0

                        def increase_inventory(self, amount):
                            self.inventory += amount

                        def reduce_inventory(self, amount):
                            self.inventory -= amount

                        def get_label(self):
                            return "Foxolate Shop: " + self.name

                        def get_inventory_report(self):
                            if self.inventory == 0:
                                return "There are no bars!"
                            return f"There are {self.inventory} bars."
                    

                    class Product {

                        std::string name;
                        double price;
                        std::string nutrition;
                        int inventory;

                        Product(std::string name, double price, std::string nutrition) {
                            this->name = name;
                            this->price = price;
                            this->nutrition = nutrition;
                            this->inventory = 0;
                        }

                        void increase_inventory(int amount) {
                            this->inventory += amount;
                        }

                        void reduce_inventory(int amount) {
                            this->inventory -= amount;
                        }

                        std::string get_label() {
                            return "Foxolate Shop: " + this->name;
                        }

                        std::string get_inventory_report() {
                            if (this->inventory == 0) {
                                return "There are no bars!";
                            }
                            return "There are " + std::to_string(this->inventory) + " bars.";
                        }
                    };
                    

Class


                    class Product {

                        std::string name;
                        double price;
                        std::string nutrition;
                        int inventory;

                        Product(std::string name, double price, std::string nutrition) {
                            this->name = name;
                            this->price = price;
                            this->nutrition = nutrition;
                            this->inventory = 0;
                        }

                        void increase_inventory(int amount) {
                            this->inventory += amount;
                        }

                        void reduce_inventory(int amount) {
                            this->inventory -= amount;
                        }

                        std::string get_label() {
                            return "Foxolate Shop: " + this->name;
                        }

                        std::string get_inventory_report() {
                            if (this->inventory == 0) {
                                return "There are no bars!";
                            }
                            return "There are " + std::to_string(this->inventory) + " bars.";
                        }
                    };
                    

We'll explain the -> instead of the . later. Hint: this is a pointer, self is a reference.

Note the std::to_string function that converts an integer into a C++ string.

A new library format was just added to the C++ standard library with C++20. Using it, you could change the last line in get_inventory_reportp to:

return std::format("There are {} bars.", this->inventory);

Public vs Private

C++ enforces encapsulation, which means restricting direct access to some of an object's components. This is a good software engineering principle.

Recall that C++ defaults everything in the class to be private. You must explicitly use the keyword public. Many people use both public and private for clarity.

  • Note we need a colon : after private and public

                    class Product {

                    private:  // This is optional
                        std::string name;
                        double price;
                        std::string nutrition;
                        int inventory;

                    public:
                        Product(std::string name, double price, std::string nutrition) {
                            this->name = name;
                            this->price = price;
                            this->nutrition = nutrition;
                            this->inventory = 0;
                        }

                        void increase_inventory(int amount) {
                            this->inventory += amount;
                        }

                        void reduce_inventory(int amount) {
                            this->inventory -= amount;
                        }

                        std::string get_label() {
                            return "Foxolate Shop: " + this->name;
                        }

                        std::string get_inventory_report() {
                            if (this->inventory == 0) {
                                return "There are no bars!";
                            }
                            return "There are " + std::to_string(this->inventory) + " bars.";
                        }
                    };
                    

Class Usage Comparison

Python


                    pina_bar = Product("Piña Chocolotta", 7.99, "200 calories")
                    pina_bar.increase_inventory(2)
                    

C++ (2 ways)


                    Product pina_bar("Piña Chocolotta", 7.99, "200 calories");
                    pina_bar.increase_inventory(2);
                    

                    Product* pina_bar = new Product("Piña Chocolotta", 7.99, "200 calories");
                    pina_bar->increase_inventory(2);
                    

C++ forces the programmer to think about where variables are stored in memory.

  • In the first code snippet, the object is allocated on the stack
  • In the second code snippet, the object is allocated on the heap, which is indicated by using the keyword new
  • In the Python code snippet, the object is allocated on the heap — that is the only option in Python — even though the syntax is more similar to the C++ object allocated on the stack

Class Usage Comparison

C++ (2 ways)


                    Product pina_bar("Piña Chocolotta", 7.99, "200 calories");
                    pina_bar.increase_inventory(2);
                    

                    Product* pina_bar = new Product("Piña Chocolotta", 7.99, "200 calories");
                    pina_bar->increase_inventory(2);
                    

The second C++ code snippet has Product* which indicates a pointer to memory that contains a variable of type Product. Calling new always returns a pointer.

⚠️ An object allocated on the stack will be lost once it goes out of scope (when the program reaches the next closing brace }).

⚠️ An object allocated on the heap must be deallocated, using the keyword delete otherwise you will have a memory leak ⚡ (unrecoverable memory). Memory leaks affect efficiency and cause problems the longer a program runs.


                    delete pina_bar;  // once we are done using it, or the program is closing
                    

Pointers

Pointers

Pointers are variables that store a memory address. That address is the memory location to an actual variable with some value. Thus a pointer points to a variable.


                    int x = 13;
                    int* x_ptr = &x;   // The '&' gets the memory address
                    *x_ptr = 5;            // The '*'dereferences the pointer and accesses the memory

                    printf("x is %d\n", x);            // "x is 5"
                    printf("x_ptr is 0x%x\n", x_ptr);  // "x_ptr is 0xf993b534"
                    printf("*x_ptr is %d\n", *x_ptr);  // "*x_ptr is 5"
                    

The value of the pointer, or memory address may change each time the program is run.

Pointers in Python

This is confusing at first, but recall that when we looked at Python lists in the environment, the name points to the actual list. Python hides pointers so the programmer doesn't have to worry about the. C++ does not.

Python List:


                    pair = [1, 2]
                    

pair is a pointer! 😖

Python hides the memory from the programmer, so even though there is a pointer, as the programmers we just think of pair as a variable. It is called a reference.

Pointers to Objects

When calling a method of an object via a pointer, you need to first dereference the pointer with * and next access the method with ., but since the access operator has higher precedence you must put the dereference operation in parentheses 😕


                    Product* pina_bar = new Product("Piña Chocolotta", 7.99, "200 calories");
                    (*pina_bar).increase_inventory(2);
                    

To simplify the above notation, C++ uses -> which does both the dereference and access 😆


                    Product* pina_bar = new Product("Piña Chocolotta", 7.99, "200 calories");
                    pina_bar->increase_inventory(2);
                    

Inheritance

Inheritance


                    class Animal:
                        species_name = "Animal"
                        scientific_name = "Animalia"
                        play_multiplier = 2
                        interact_increment = 1

                        def __init__(self, name, age=0):
                            self.name = name
                            self.age = age
                            self.calories_eaten  = 0
                            self.happiness = 0

                        def play(self, num_hours):
                            self.happiness += (num_hours * self.play_multiplier)
                            print("WHEEE PLAY TIME!")
                    

                    class Animal {

                        std::string name;
                        int age;
                        int calories_eaten;
                        int happiness;

                    public:
                        std::string species_name = "Animal";
                        std::string scientific_name = "Animalia";
                        int play_multiplier = 2;
                        int interact_increment = 1;
                    
                        Animal(std::string name, int age=0) {
                            this->name = name;
                            this->age = age;
                            this->calories_eaten = 0;
                            this->happiness = 0;
                        }

                        void play(int num_hours) {
                            this->happiness += (num_hours * this->play_multiplier);
                            printf("WHEEE PLAY TIME!\n");
                        }
                    };
                    

Inheritance


                    class Rabbit(Animal):
                        species_name = "European rabbit"
                        scientific_name = "Oryctolagus cuniculus"
                        calories_needed = 200
                        play_multiplier = 8
                        interact_increment = 4
                        num_in_litter = 12
                    

                    class Rabbit : public Animal {

                    public:
                        int calories_needed = 200;
                        int num_in_litter = 12;
                        
                        std::string species_name = "European rabbit";
                        std::string scientific_name = "Oryctolagus cuniculus";
                        int play_multiplier = 8;
                        int interact_increment = 4;
                        
                        Rabbit(std::string name, int age=0) : Animal(name, age) {}
                    };
                    

Note that we must explicitly define a constructor for the subclass, Rabbit, and that it must call the superclass constructor.

It is bad practice to have class data public. This is one of the major complaints against Python.

Common practice is to use getter and setter methods that allow the class to manage the data at a private level, but still provide a way to read from and/or write to the data.

Getters and Setters

Here is the Rabbit class using getters and setters.


                    class Rabbit : public Animal {

                        int calories_needed = 200;
                        // ... other data members (private)
                    
                    protected:
                        std::string species_name = "European rabbit";
                        // ... other inherited data members (protected)
                    
                    public:
                        Rabbit(std::string name, int age=0) : Animal(name, age) {}

                        int get_calories_needed() { return calories_needed; }
                        // ...
                        std::string get_species_name() { return species_name; }
                        // ...

                        void set_calories_needed(int calories_needed) {
                            if (calories_needed < 1) {
                                throw std::invalid_argument("calories_needed must be greater than 1");
                            }

                            this->calories_needed = calories_needed;
                        }
                        
                    };
                    

Using getter and setter methods adds a lot more code to write, but it facilitates encapsulation and allows for validation (like in the setter above).

This are very important when building large software systems.

protected Keyword

The protected keyword is used to indicate data that can be access by the class and any of its subclasses, but no publicly.


                    // Old
                    class Animal {

                        std::string name;
                        int age;
                        int calories_eaten;
                        int happiness;

                    public:
                        std::string species_name = "Animal";
                        std::string scientific_name = "Animalia";
                        int play_multiplier = 2;
                        int interact_increment = 1;

                        Animal(std::string name, int age=0) {
                            this->name = name;
                            this->age = age;
                            this->calories_eaten = 0;
                            this->happiness = 0;
                        }
                    };
                    

                    // New
                    class Animal {

                        std::string name;
                        int age;
                        int calories_eaten;
                        int happiness;

                    protected:
                        std::string species_name = "Animal";
                        std::string scientific_name = "Animalia";
                        int play_multiplier = 2;
                        int interact_increment = 1;

                    public:
                        Animal(std::string name, int age=0) {
                            this->name = name;
                            this->age = age;
                            this->calories_eaten = 0;
                            this->happiness = 0;
                        }
                    };
                    

protected Keyword

Getter and setter methods for protected class data should be in that class.


                    class Animal {

                        std::string name;
                        int age;
                        int calories_eaten;
                        int happiness;

                    protected:
                        std::string species_name = "Animal";
                        std::string scientific_name = "Animalia";
                        int play_multiplier = 2;
                        int interact_increment = 1;

                    public:
                        Animal(std::string name, int age=0) {
                            this->name = name;
                            this->age = age;
                            this->calories_eaten = 0;
                            this->happiness = 0;
                        }

                        std::string get_species_name() { return species_name; }
                        std::string get_scientific_name() { return scientific_name; }

                        // No setters for `species_name` or `scientific_name`
                        //   since they shouldn't change
                    };
                    

Not all data members need getters and setters. In this example, we may want subclasses to have access to play_multiplier and interact_increment, but not provide a way for outside code to read from or write to those fields.

Inheritance and Pointers

When using inheritance and vectors, you must use pointers.


                    std::vector animals;

                    animals.push_back(new Rabbit("Bugs"));
                    animals.push_back(new Elephant("Ella"));
                    animals.push_back(new Lion("Leo"));
                    

Don't forget to deallocate any allocated memory 👀


                    for (Animal* animal : animals) {
                        delete animal;
                    }
                    animals.clear();
                    

Passing Arguments

Passing Arguments

Python uses both pass by value and pass by reference by default — Python does the first for primative types and the second for references.

When you pass arguments, or parameters, to a function in C++, the default is to use pass by value.

Essentially, Python does what a programmer would want by default, but with C++ the programmer must explicity say when to pass by reference 🙄

Pass by Value

Pass by value works as expected for primitive types (int, bool, etc.) and for arrays and pointers.

Since pass by value always makes a copy of the variable to pass to the function, passing an object by value is usually not what you want.

  • The original object will not be modified once the scope of the function is left
  • The entire object has to be copied on the stack which is slow and memory inefficient

Passing object by reference solves both of these problems.

Pass by Value: Objects


                    #include <cstdio>
                    #include <vector>
                    
                    void zeroize(std::vector<int> values) {
                        for (int i = 0; i < values.size(); i++) {
                            values[i] = 0;
                        }
                    }

                    int main() {
                        std::vector<int> values;
                        values.push_back(8);
                        values.push_back(13);
                        values.push_back(15);

                        zeroize(values);

                        // will NOT print 0, but instead will print 15
                        printf("Last should be zero, is: %d\n", values[values.size()-1]);
                        return 0;
                    }
                    

The vector values is copied and then passed to the function zeroize.

The zeroize function does modified the copied vector to only contain three zeros.

The zeroize function does NOT modify the vector values defined in main.

Pass by Reference: Objects


                    #include <cstdio>
                    #include <vector>
                    
                    void zeroize(std::vector<int>& values) {
                        for (int i = 0; i < values.size(); i++) {
                            values[i] = 0;
                        }
                    }

                    int main() {
                        std::vector<int> values;
                        values.push_back(8);
                        values.push_back(13);
                        values.push_back(15);

                        zeroize(values);

                        // will print 0
                        printf("Last should be zero, is: %d\n", values[values.size()-1]);  
                        return 0;
                    }
                    

Note the use of & after the type of the parameter to indicate pass by reference.

The zeroize function now modifies the vector values defined in main.

The zeroize function parameter name doesn't matter. It is the & that matters.

End of Line

This ends your crash course on C++ 💥