Custom types and classes
One of the greatest powers of C++ is the level of abstraction it allows the programmer use when creating code.
Abstractions are important to make huge programs easier to understand, evolve and fix possible issues.
Classes are the foundational idiom for that, let's understand why.
A matter of state and messages
Dealing only with structured paradigm, you have functions, primitive types, arrays and structs to express the program.
While types can express state, functions can perform operations on that state. Often functions can also call other functions, reusing operations and combining them to produce more complex transformations.
That part, the graph of function calls in a program, is the message passing.
The following example shows those concepts in action in a simple todo list application:
#include <iostream>
#include <cstdlib>
struct TodoItem
{
std::string description;
bool completed;
struct TodoItem *next;
struct TodoItem *prev;
};
void addItem(TodoItem *&head, const std::string &description)
{
TodoItem *newItem = (TodoItem *)malloc(sizeof(TodoItem));
newItem->description = description;
newItem->completed = false;
newItem->next = nullptr;
newItem->prev = nullptr;
if (!head)
{
head = newItem;
}
else
{
TodoItem *temp = head;
while (temp->next)
{
temp = temp->next;
}
temp->next = newItem;
newItem->prev = temp;
}
}
void markCompleted(TodoItem *head, int index)
{
TodoItem *temp = head;
for (int i = 0; temp && i < index; ++i)
{
temp = temp->next;
}
if (temp)
{
temp->completed = true;
}
}
void displayList(TodoItem *head)
{
TodoItem *temp = head;
int index = 0;
while (temp)
{
std::cout << index++ //
<< ". [" << (temp->completed ? 'x' : ' ') //
<< "] " << temp->description << '\n';
temp = temp->next;
}
}
void freeList(TodoItem *head)
{
TodoItem *temp;
while (head)
{
temp = head;
head = head->next;
free(temp);
}
}
int main()
{
TodoItem *todoList = nullptr;
addItem(todoList, "Buy groceries");
addItem(todoList, "Walk the dog");
addItem(todoList, "Read a book");
std::cout << "Todo List:\n";
displayList(todoList);
std::cout << "\nMarking item 1 as completed.\n";
markCompleted(todoList, 1);
std::cout << "\nUpdated Todo List:\n";
displayList(todoList);
freeList(todoList);
return 0;
}Some common patterns emerge when doing it properly:
One specific function to create state
The struct defines a shape for the state, but it's up to the functions define how the data must be initialized.
Distinct messages will manipulate distinct aspects of the state
The function to mark the todo item as complete only needs to know how to flip the flag, but all the struct is exposed to it. On large scale projects this is not desirable.
Message implement behavior
One function creates a todo item, another function prints the todo list and so on. Those messages perform transformations over the state, either changing it or creating some side effect. This is behavior.
Discipline when programming
All of this is accomplished with bare, simple structures and a lot of discipline from the programmer.
All kinds of abstractions can be achieved by simply 'coding right'.
But it demands experience.
Another approach is to create foundational language structures to enforce code quality. Ths is where the fun begins.
Basic class declaration and class usage
You can think of classes as a specification of states and messages that are supposed to be deeply related:
#include <iostream>
class TodoItem
{
std::string description;
bool completed;
TodoItem *next;
TodoItem *prev;
public:
TodoItem *toNext();
TodoItem *toPrev();
void complete();
static void display(TodoItem *head);
static void addItem(TodoItem *&head, const std::string &description);
};
void TodoItem::complete()
{
completed = true;
}
TodoItem *TodoItem::toNext()
{
return next;
}
TodoItem *TodoItem::toPrev()
{
return prev;
}
void TodoItem::display(TodoItem *head)
{
TodoItem *temp = head;
int index = 0;
while (temp)
{
std::cout << index++ //
<< ". [" << (temp->completed ? 'x' : ' ') //
<< "] " << temp->description << '\n';
temp = temp->next;
}
}
void TodoItem::addItem(TodoItem *&head, const std::string &description)
{
TodoItem *newItem = new TodoItem();
newItem->description = description;
newItem->completed = false;
newItem->next = nullptr;
newItem->prev = nullptr;
if (!head)
{
head = newItem;
}
else
{
TodoItem *temp = head;
while (temp->next)
{
temp = temp->next;
}
temp->next = newItem;
newItem->prev = temp;
}
}
int main()
{
TodoItem *head = nullptr;
TodoItem::addItem(head, "Learn C++");
TodoItem::addItem(head, "Build a project");
TodoItem::addItem(head, "Contribute to open source");
head->toNext()->complete(); // Mark the second item as completed
TodoItem::display(head);
return 0;
}Visibility qualifiers
Unlike bare, naked structs, the attributes declared on it aren't accessible directly. This is called encapsulation.
In the class declaration, anything you want to be available for external usage must be declared in the public section:
class MyClass {
int myPrivateState;
public:
int myPublicMessage();
};Constructor and destructors
Classes allow special functions for the sole purpose of set the initial state. Also, it's possible to set a special function just for resources cleanup. Those are the Constructor and Destructor functions:
class MyClass
{
int myPrivateState;
public:
int myPublicMessage();
// this function is a constructor
MyClass();
// this one is a destructor
~MyClass();
};
// function implementation
MyClass::MyClass()
{
}
// function implementation
MyClass::~MyClass()
{
}How exactly it works?
#include <iostream>
#include <cstring>
class TodoItem
{
// this time our public interface for this class comes first.
public:
TodoItem(const char *description);
~TodoItem();
void complete();
std::string display();
// to declare private state later, jus add the private section
private:
char *description;
bool completed;
};
TodoItem::TodoItem(const char *desc)
{
// allocate memory for description
description = new char[strlen(desc) + 1];
strcpy(description, desc);
completed = false;
std::cout << "Created todo item: " << this << std::endl;
}
TodoItem::~TodoItem()
{
// the memory we allocated gets freed here
free(description);
std::cout << "Deleted todo item: " << this << std::endl;
// this is a special pointer to the current object
}
void TodoItem::complete()
{
completed = true;
}
std::string TodoItem::display()
{
return std::string(completed ? "[x] " : "[ ] ") + description;
}
int main()
{
// these goes on stack
TodoItem item1("Learn C++ classes");
TodoItem item2("Build a todo list app");
// this goes on heap. the new operator returns a pointer to the allocated memory
TodoItem *item3 = new TodoItem("heap the pointer");
std::cout << item1.display() << std::endl;
std::cout << item2.display() << std::endl;
std::cout << item3->display() << std::endl;
item1.complete();
std::cout << item1.display() << std::endl;
std::cout << item2.display() << std::endl;
std::cout << item3->display() << std::endl;
// since item3 was allocated on the heap, we need to free it manually
delete item3;
// after the function ends, item1 and item2 go out of scope and their
// destructors are called automatically
return 0;
}By the way, distinct values of the same class are called objects or class instances.
Function and Operator overloading
Another abstraction provided by C++ is called polymorphism. It's a way to group similar behavior under a common name.
Take the complete() method/function/message/behavior for example:
void TodoItem::complete()
{
completed = true;
}What if you want a bew behavior, one to mark the item as not completed?
You can either create a pending() function, or any other name, or could add an argument to the existing method.
But by doing the signature change (signature, in short, is the function name plus its arguments), other places in the code already using it would need to be changed as well.
Instead, just declare an overload to the complete() method:
//