Week 4
In this and subsequent lectures we will start learning C++ while continuing to practice most of what we learned about C. Today we will review object-oriented programming concepts and the C++ language features that support them.
Object-Oriented Programming
In pure OO languages:
- Everything is an object. Think of an object as a fancy variable; it stores data, but you can “make requests” to that object, asking it to perform operations on itself. In theory, you can take any conceptual component in the problem you’re trying to solve (dogs, buildings, services, etc.) and represent it as an object in your program.
- A program is a bunch of objects telling each other what to do by sending messages. To make a request of an object, you “send a message” to that object. More concretely, you can think of a message as a request to call a function that belongs to a particular object.
- Each object has its own memory made up of other objects. Put another way, you create a new kind of object by making a package containing existing objects. Thus, you can build complexity in a program while hiding it behind the simplicity of objects.
- Every object has a type. Using the parlance, each object is an instance of a class, in which “class” is synonymous with “type.” The most important distinguishing characteristic of a class is “What messages can you send to it?”
- All objects of a particular type can receive the same messages. This is actually a loaded statement, as you will see later. Because an object of type “circle” is also an object of type “shape,” a circle is guaranteed to accept shape messages. This means you can write code that talks to shapes and automatically handles anything that fits the description of a shape. This substitutability is one of the most powerful concepts in OOP.
C++ is not quite as pure. Some types in C++ define variables that are not objects, e.g., all the base types -- int, bool, float, double, long, etc. You can also have functions outside of classes, enum types, templates and a few other concepts that don't fit the pure OO notion above. Many (but not all) non-OO features of C++ are due to the legacy of C, with which it initially C++ was backward compatible (this is no longer the case as of the C99 standard). Stroustrup (the creator of C++) explains the motivations for his approach in this article.
Abstraction
- Abstraction
- The ability to represent concepts directly in a program and hide incidental details behind well-defined interfaces – is the key to every flexible and comprehensible system of any significant size.
An abstract class:
- class Animal {
- public:
- Animal();
- ~Animal();
- void eat();
- void sleep();
- void drink();
- bool isCarnivorous() { return carnivorous; }
- protected:
- int legs;
- int arms;
- int age;
- bool carnivorous;
- };
Encapsulation
- Encapsulation
- The ability to provide guarantees that an abstraction is used only according to its specification (i.e., interface), making it harder to corrupt the internal state of an object or use it improperly.
In the Animal example, we can only access instances of Animal through the eat()
, sleep()
, and drink()
functions.
Data hiding
- public
The most open level of data hiding is public. Anything that is public is available to all derived classes of a base class, and the public variables and data for each object of both the base and derived class is accessible by code outside the class. Everything following is public until the end of the class or another data hiding keyword is used.
In general, a well-designed class will have no public fields--everything should go through the class's functions.
- protected
Variables and functions marked protected are inherited by derived classes; however, these derived classes hide the data from code outside of any instance of the object. Keep in mind, even if you have another object of the same type as your first object, the second object cannot access a protected variable in the first object. Instead, the second object will have its own variable with the same name, but not necessarily the same data.
- private
Functions and variables marked private are not accessible by code outside the specific object in which that data appears. Private variables and functions are not inherited by derived classes.
Inheritance
- Inheritance
- The ability to compose new abstractions from existing ones.
- class Quadruped : public Animal {
- public:
- Quadruped() : Animal() { legs = 4; }
- void walk();
- void run();
- };
- class Sheep : public Quadruped {
- public:
- Sheep() : Quadruped() { carnivorous = false; }
- void growWool();
- };
- class Wolf : public Quadruped {
- public:
- Wolf() : Quadruped() { carnivorous = true; }
- void hunt();
- };
Is-a vs. is-like-a relationships
vs. | ||
Is-a relationship | Is-like-a relationship |
- Is-a
- Inheritance overrides only base-class functions (and do not add new member functions that aren’t in the base class). Hence, the derived type is exactly the same type as the base class since it has exactly the same interface.
- Is-like-a
- The derived class adds to the parent class interface, creating a new type (e.g., see the
Sheep
andWolf
classes, each of which adds methods to the originalQuadruped
interface). The new functions are not accessible from the parent class.
Inheritance vs Composition
One of the fundamental activities of an object-oriented design is establishing relationships between classes. Two fundamental ways to relate classes are inheritance and composition.
The Sheep
class above demonstrates inheritance. Here is a modified version that gives an example of composition:
- class Color {
- public:
- Color();
- ~Color();
- string getName();
- void setName(string);
- private:
- string name;
- };
- class Wool {
- public:
- Wool(Color);
- ~Wool();
- Color getColor();
- private:
- Color color;
- };
- class Sheep : public Quadruped {
- public:
- Sheep() : Quadruped() { carnivorous = false; }
- void growWool(Color);
- private:
- Wool wool;
- };
Polymorphism
- Polymorphism
- The ability to send a message to an object without knowing what its type is.
An everyday life example of polymorphism is the ability to drive almost any car without knowing who built the car or how it was built. In each car you can start, accelerate, break, turn, etc pretty much the same way.
A closer look into the C / C++ preprocessor
So far we have seen how to define preprocessor variables (with #define
), check whether a variable is defined (with #ifdef
) or not defined (with #ifndef
). You can do more in preprocessor macros, such as have arguments:
#define min (X, Y) ((X) < (Y) ? (X) : (Y))
Things to keep in mind (or why macros can be a VERY BAD IDEA). If you must, read enough to do it well, e.g., look at this C Preprocessor FAQ and examples of the worst real-world preprocessor abuse.
- The preprocessor replaces all macro names with their definitions -- so in the above example, wherever it encounters code that looks like
min(a,b)
, it will substitute ((a) < (b) ? (a) : (b)). Why is this bad? We have no namespace capabilities or symbol resolution (remember that the compiler handles that). So if some real function (not the macro) has the same name (e.g., amin(m,n)
function defined in some class or in the global scope), it will get replaced and you will get the wrong code. - Macros are not visible in the debugger, you only get information about the code the compiler saw (if you compiled with
-g
), which makes it very hard to find the place where you need to fix something. - Macros can get very ugly, very fast. Resist the temptation to do cool things with macros! Some examples of malicious or just plan ugly things to do:
- // Never, ever assume a macro is what its name is implying
- #define True 0
- #define False 1
- #define FileNotFound sqrt(False)
- // This one is pretty common (unfortunately)
- #define RETURN(result) return (result);}
- // Can you figure out what this actually does?
- #include <iostream>
- #define System S s;s
- #define public
- #define static
- #define void int
- #define main(x) main()
- struct F{void println(char* s){std::cout << s << std::endl;}};
- struct S{F out;};
- public static void main(String[] args) {
- System.out.println("Hello World!");
- }
- // Just "No".
- #define private public
- // Sometimes you may not have a choice
- #if 0
- Everything here will not be visible to the compiler
- #endif
C++ Namespaces
In C++ you can add an extra layer of encapsulation -- namespaces, which can contain many classes, functions, types, etc.
The std
namespace contains the C++ standard library.
Namespaces add extra protection against name collisions at link time. You can "fake" namespaces in C, but it results in very long, unreadable names.
C++ function declarations
There is a significant difference between C and C++ when declaring functions with empty argument lists. In C, the declaration:
int foo();
can be used for declaring foo which can have any number and types of arguments. However, in C++ it means “a function with no arguments.”
If you want to specify that a function you are using in C++ is a C function, use extern "C"
:
extern "C" {
int foo();
}
Standard C++ include format
Don't specify the suffix for standard headers, i.e., #include <iostream>
When you include C headers, use no suffix and prepend 'c', e.g., #include <cstdlib>
(instead of #include <stdlib.h>
C++ input/output
- Streams
- Output: "Hello world!"
- Reading input
- Working with files
Dynamic memory allocation in C++
By now you are used to malloc and free. In this part of the lecture we will see how to allocate and free memory with the C++ language constructs.
The new() operator
Simple types:
- int *intptr = new int;
- int *array = new int[10]; // an array of ints
- int **array2D = new int*[5]; // a 2-D array of ints
- for (int i = 0; i < 5; i++)
- array2D[i] = new int[5];
The delete operator
As with malloc and free, you have to deallocate any memory you have allocated with new()
, which you can do with the delete
operator.
- delete intptr;
- delete []array;
- for (int i = 0; i < 5; i++) delete []array2D[i];
- delete []array2D;
Complete example.
Reducing recompilation
Apart from readability, one of the main reasons for putting different class implementations in different files is that if you do that and use a Makefile, it will only compile the file which changed -- and a small file is compiled much faster than a very large one.
Exercise: split up the animals.cpp code into separate cpp files and measure how long it takes to compile when a single file is changed vs the monolithic animals.cpp compile time
Unix minute
UNIX is now available to you on most devices, even Windows (10 and later) provides a native Ubuntu-based environemnt. Consider that not too long ago, this is what it took to get Unix on your Windows machine.