C++
Preprocessing
Preprocessing is the phase of executing preprocessing directives in a source file, which are then removed from the resulting translation unit, which combines the pure C or C++ code of a source file with all its included header files. The translation unit is then compiled into an object file, and it is the linker that then forms linkages between object files to produce an executable module.
The #define
directive specifies a macro which can define a text replacement to occur in code before it is compiled. Macros are considered a holdover from C, and other constructs like variable templates and function templates are considered better suited in C++. In practice, #define
statements are most commonly used to handle header files.
Here, any instance of "PI
" in the source code will be replaced by the string of digits "3.14159265", but it will not be replaced if it forms part of an identifier or appears in a string literal or comment.
#define PI 3.14159265
#define break
#define PI 3.14\
159265
Function-like macros are possible because the preprocessor can recognize a function call in the macro identifier and replace its arguments intelligently. Here any invocation of the MAX()
function call will have its arguments incorporated into the substitution statement.
#define MAX(A, B) A >= B ? A : B
All the lines following #ifndef
(#if !defined
) will be kept in the file as long as the identifier "MYHEADER_H" has not already been defined. This common is called an #include
guard.
#ifndef MYHEADER_H
#define MYHEADER_H
// ...
#endif
Variables
Since C++14, you can separate digits in a long integer with a single quote to make it more readable.
int num {12'345}; // 12,345
int hex {0xabcdef};
int oct {0567};
constexpr
is used in some situations I can't figure out yet.
static constexpr u32 MAX_MEM = 1024 * 64;
size_t
is a type alias defined in the Standard Library (in the cstddef
header). It is an alias for an unsigned integer type.
Initialization
A braced initializer refers to placing the initial value of a variable in braces. This is a novel style of initialization introduced in C++11. Its main advantage is that it will raise a compile-time error if the compiler has to perform a narrowing conversion of the value to match the declared type.
int apples {15};
Older but equally valid ways of initializing variables:
int oranges = 12;
int kiwis(13); // "functional notation"
int grapes {}; // 0
Sequences, like this array class can be efficiently zero-initialized; the braces can contain any number of values up to the declared size of the array (remaining values will be zero-initialized).
#include <array>
array<int, 5> myIntArray{};
int myNums[2][3]
{
{1, 2, 3},
{4, 5, 6}
};
Pointers
Smart pointers (also called managed pointers) are pointers that manage their own memory. They were introduced in C++11, and there is no longer a reason to use the earlier raw pointers. In older versions of C++, memory leaks were common because the programmer had to remember to release memory allocated dynamically from the heap/free store using the delete
keyword. These older pointers are now called raw pointers
The most commonly used smart pointer is unique_ptr
: others include shared_ptr
and weak_ptr
.
Only a single unique_ptr<T>
can point to any memory address, unless ownership is transfered with move()
.
Since C++14, it is recommended to create unique pointers using makeunique<T>()
.
auto pdbl = make_unique<dbouble>();
When variables are declared with an asterisk *
appended to the datatype or prepended to the identifier, the variable becomes a pointer to that type.
Pointer identifiers usually begin with "p", a convention known as Hungarian notation.
The size of pointers corresponds to the address space of available memory (4 bytes for 32-bit architectures, and 8 bytes for 64-bit).
// The following statements are equivalent.
long* pnum {};
long *pnum {nullptr};
void *
is known as "pointer to void type", meaning variables defined as such are pointers to data of an unspecified type, making it similar to var
in C#.
The address-of operator &
obtains the address of a variable. The address-of operator typically also occurs with the indirection operator or dereference operator (also *
) to access the data pointed to by a pointer. Using a dereferenced pointer is the same as using the variable to which it points.
long num {12345L};
long* pnum {&num};
long newnum {*pnum + 1};
vector<T>
container , the indirect member selection operator (->
) can be used to access the methods.
// The following statements are equivalent.
auto* pdata {new std::vector<int>{}};
std::vector<int> data;
auto* pdata = &data;
// The following statements are equivalent.
(*pdata).push_back(66);
pdata->push_back(66);
Pointers to classes can be recast with the following syntax:
Animal* ptr = &kitty;
((Cat*)ptr)->chaseMouse();
// newer, safer syntax
(reinterpret_cast<Cat>(ptr))->chaseMouse();
Containers
Containers are a type of data structure used to contain elements for various purposes. They are deeply tied to algorithms through iterators.
Two array-like data structures defined in the Standard Library that are more typically used are array<T,N>
and vector<T>
Arrays
An array is a variable that represents a contiguous sequence of memory locations, each storing an item of data of the same data type, each of which are called elements. Arrays must be declared with a constant integer expression that is fixed at compile time. Built-in arrays in C++ are inherited from C.
int primes[10] {1, 2, 3, 5, 7, 11, 13, 17, 19, 23}
Sequence containers
The two most common sequence containers are array<T,N>
and vector<T>
.
All sequence containers expose several of a family of related member functions:
Member function | vector | array | list | forward_list | deque |
---|---|---|---|---|---|
front() |
✅ | ✅ | ✅ | ✅ | ✅ |
back() |
✅ | ✅ | ✅ | ❌ | ✅ |
push_front() |
❌ | ❌ | ✅ | ✅ | ✅ |
pop_front() |
❌ | ❌ | ✅ | ✅ | ✅ |
push_back() |
✅ | ❌ | ✅ | ❌ | ✅ |
pop_back() |
✅ | ❌ | ✅ | ❌ | ✅ |
insert() |
✅ | ❌ | ✅ | ✅ | ✅ |
erase() |
✅ | ❌ | ✅ | ✅ | ✅ |
array<T,N>
(also "array class") is a fixed sequence defined with two template parameters to create an array of N
elements of type T
. Here it is zero initialized with an empty braced initializer.
#include <array>
array<int, 5> myIntArray{};
fill()
: Set every element of the array to the same value
- size()
: Return the number of elements as a type size_t
- at()
: Access an element at a given index but testing for a valid range. Safer than using the built-in index method.
Vectors are sequential containers with typed elements like the array class, but are not limited to fixed sizes. The push_back()
method is similar to a Python List.append()
. Other methods like front()
, back()
, and pop_back()
can be used to manipulate the vector.
#include <vector>
vector<int> vals;
insert()
method takes two arguments, one is an iterator, here provided by yet another vector method - begin()
, and the content to be inserted. This code will insert the string at index 2.
#include <iostream>
#include <vector>
#include <string>
using namespace std;
int main() {
vector<string> family;
family.push_back("Plato");
family.push_back("Aristotle");
family.push_back("Socrates");
family.push_back("Pythagoras");
family.push_back("Aristarchos");
family.insert(family.begin() + 2, "Dgiapusccu");
family.pop_back();
for (string i : family) {
cout << i << endl;
}
return 0;
}
A forward_list<T>
is an implementation of the singly-linked list and is rarely used.
A list<T>
is an implementation of the doubly-linked list and is rarely used.
The double-ended queue (deque) exposes push_Front()
and push_back()
methods.
Container adapters
A stack<T>
implements last-in first-out (LIFO) semantics.
Stacks support push
and pop
methods.
A queue<T>
implements first-in first-out (FIFO) semantics.
Associate containers
Standard iterators
There are three types of iterator supported by containers in the Standard Library:
- random access iterator support the widest variety of operations: vector<T>
, array<T,N>
, and deque<T>
- forward iterators do not support decrement operations ("going backwards"), and this describes the operation of a forward_list<T>
- bidirectional iterators support both increment and decrement operations, but cannot jump more than one value, describing the operation of a list<T>
stack<T>
, queue<T>
, and priority_queue<T>
do not have iterators whatsoever.
Containers in the Standard Library expose a begin()
member function, which is the most commonly used iterator.
std::vector<char> letters{'a','b','c','d','e'}
auto iter{letters.begin()};
// Specifying type explicitly
std::vector::iterator iter{letters.begin()};
std::cout << *iter << std:: endl; // a
++iter;
std::cout << *iter << std:: endl; // b
A string can be reversed using the rbegin()
and rend()
iterators:
string name{"Lorem ipsum..."};
string reverse(name.rbegin(), name.rend());
Maps
A syntactic sugar has been available since C++17:
for ([x, y] : coords)
{
std::cout << x << y << endl;
}
for (std::pair<int, int> el : coords)
{
std::cout << el.first << el.second << endl;
}
pair
type has two public fields:
first
second
## Math
#include <math.h>
using namespace std;
int main() {
int num{2};
cout << pow(num,2) << endl;
}
Classes
New data types in C++ are created as classes, which can be composites of member variables of other types and member functions, allowing complex and intuitive models to be created.
The three primary principles of OOP are:
- Encapsulation: member variables and functions are packaged together
- Data hiding preserves the integrity of an object
- Inheritance allows one type to define another.
- Polymorphism (in C++ implemented by calling member functions using a pointer or reference) allow behavior of base classes to be exposed from objects of derived classes
Member variables
Classes can contain member variables that are public
or private
("access specifiers"), but it is best practice to make variables private while implementing accessor functions (getters and setters):
- Hiding data preserves the integrity of objects
- Loose coupling facilitates future change in codebase
- Extra code can be injected for logging or data validation
- Debuggers can set breakpoints on these getters and setters
class Box
{
private:
double length {1,0};
double width {1,0};
double height {1,0};
public:
double volume()
{
return length * width * height;
}
};
Initialization
A member initializer list can be used to initialize fields more efficiently than explicit assignment.
Box::Box(double lv, double wv, double hv) : length {lv}, width {wv}, height {hv} {}
// Compiler error
Box::Box(double lv, double wv, double hv) : length {lv}, width {wv}, height {hv}
class Animal
{
public:
std::string _name{};
Animal(std::string n) : _name {n} {}
}
class Dog : public Animal
{
public:
Dog(std::string n, std::string b) : Animal(n) {_breed = b;};
}
A class constructor is called whenever a new instance of the class is defined. It always has the same name as the class itself and has no return data type because it returns no data.
class Box
{
private:
// ...
public:
Box( double l, double w, double h)
{
length = l;
width = w;
height = h;
}
};
If a constructor isn't defined, the compiler will supply a default default constructor when an object is instantiated without initial values. To define a default constructor:
Box() = default;
A destructor is a special member of a class executed to deal with cleanup upon use of the delete
operator. A class can have only one destructor, and if one isn't defined then the compiler provides a default destructor that does nothing. The name of the destructor for a class is always the class name prefixed with a tilde, and similar to a constructor it cannot have a return type or parameters.
~Box() = default;
Box::~Box() = default; // when defined outside the class
Base class destructors should always be declared as virtual
.
Access
When variables of class types are instantiated with the const
keyword, they are called const objects, and none of their member variables can be altered (member variables of const objects become immutable). The compiler will throw an error when attempting to invoke methods of const objects unless they are identified as const member functions by using the const
keyword in the signature after the identifier ("attribute"?):
class Box {
double volume () const { /* ... */ }
double getLength() const { return length; }
double getWidth() const { return width; }
double getHeight() const { return height; }
}
public
, private
, and protected
are access specifiers that determine how a member variables and functions can be accessed from the outside. When inheriting from a base class, an access specifier can also be used to determine how accessible that base class's members are within the derived class.
class Dog : public Animal
{
// ...
}
public
, inherited members are unchanged
- When the base class specifier is protected
, inherited public and protected members become protected
- When the base class specifier is private, all inherited members become private.
A friend is a function to which a class grants access to its private internals. They may be useful in rare situations where a single function needs access to the internals of different kinds of objects.
Inheritance
When creating subclasses, you must remember:
- Private variables must be placed in the protected
access specifier so that they are accessible to child classes.
- The base class access specifier must allow access to the base class's private variables (public
or protected
)
- Parent class must have a default constructor
class Animal {
protected: // not `private:`!
std::string _name;
public:
Animal() = default;
}
class Dog : public Animal { /* ... */ } //
Polymorphism
Polymorphism in C++ refers to the practice of invoking a base class's member function rather than the derived class.
Because the compiler performs early binding by default, a pointer typed to a base class but initialized to a derived class will invoke the base class's member function. Late binding can be used to force the pointer to use the derived class's member function even when the type of the pointer is the base class. This is done by using the virtual
keyword on the base class's member function.
Classes with virtual functions are called abstract classes and may not be instantiated. Abstract classes that are made of only virtual functions are called interfaces.
class Base
{
public:
virtual void doStuff() { /* ... */ }
}
class Derived : public Base
{
public:
void doStuff() { /* ... */ }
}
int main()
{
Derived derived{};
Base* pointer = &derived; // a pointer to an abstract class **may** be used
pointer->doStuff();
}
override
keyword.
class Derived : public Base {
override doStuff() // ...
}
Enumerations
Enumerations can be specified with enum
. Without specifying a value, each element of the enum is given a successively greater integer value starting with 0, like the indexes of an array (an ordinal value).
enum Choices {A, B, C, D }
::
cout << A; // 0
cout << Choices::A; // 0
Templates
Templates are used to have the compiler generate code automatically for a given data type. This is to avoid highly repetitive overloaded function definitions which only differ in parameter lists.
The template
and typename
keywords define a template. The placeholder "T" represents the data type that will be replaced by a specific type by the compiler.
template <typename T> T larger (T a, T b) {
return a>b ? a : b;
}
template <typename T1, typename T2> bool larger (T1 a, T2 b) {
return a>b;
}
Control flow
The choices in a switch
statement are called cases. You can only switch on constant expressions that can be evaluated at compile-time, typically literals but excluding strings. Each case must be followed by a break
statement to prevent fallthrough, except for the default case.
switch (choice)
{
case 1:
// ...
break;
case 2:
// ...
break;
default:
// ...
}
Since C++11, the range-based for-loop is available, which works very similar to a Python for-in loop:
for (string num : nums)
{
cout << num << endl;
}
Functions
Function prototypes, defining the function header (return data type, function name, and parameter list), describe a function sufficiently for the compiler to be able to compile calls to it and are required before using a function if the function declaration doesn't precede all the locations where it's called.
#include <iostream>
using namespace std;
// Without this prototype, there is a compile-time error.
void printSomething();
int main() {
printSomething();
return 0;
}
void printSomething()
{
cout << "something..." << endl;
}
int func(int a) {
// Pass by value
}
int func(int &a) {
// Pass by reference
}
Recursion
Recursion requires a base case and at least one recursive case. The call stack is a stack data structure that figures prominently in recursive computing.## Home
Project | Description |
---|---|
JamoftheMonthProject.cpp | CLI application that calculates how much the user owes based on selected subscription tier and units purchased |
TicTacToe.cpp | |
RPGCharGen.cpp | Multiple classes using inheritance, virtual member functions, enums |
Task | Description |
---|---|
Reverse a string | ...## Iterators |
An iterator is a classical and widespread design pattern that allows a wide variety of container-like objects to be traversed by exposing a uniform interface.
However, loops based on iterators should only be used if access to the iterator is needed for advanced processing in the loop body. A range-based for loop is the recommended way to iterate over all elements of a container.
Memory
Memory leaks can be detected using valgrind.
Namespaces
A namespace is a block that attaches an extra name to every entity name that is declared or defined within it. The qualified name of each entity is the namespace name followed by the scope resolution operator ::
followed by the basic entity name. Namespaces can be used to partition large codebases into logical groupings to avoid name clashes. If a namespace isn't defined, the global namespace, where entities have no namespace name attached, applies by default.
You can define a namespace using the namespace
keyword.
namespace foo {
// ...
}
namespace outer {
namespace inner {
void foo() {
// ...
}
}
}
outer::inner::foo()
namespace outin = outer::inner;
outin::foo()
The using
keyword allows you to reference any name from a namespace without qualifying it.
using namespace std;
using BigOnes = unsigned long long;
typedef unsigned long long BigOnes; // Older, less intuitive syntax
Operators
Each operator is associated with a particular named function. Operators can be overloaded by implementing that function.
bool Rectangle::operator==(const Rectangle& other) const
{
return _length == other._length && _width == other._width;
}
Header files
Topic | Header file |
---|---|
array | <array> |
deque | <deque> |
exception | <exception> |
map | <map> |
Mathematical functions | <math.h> |
queue | <queue> |
stack | <stack> |
vector | <vector> |
Smart pointers | <memory> |
Applications
gtkmm
gtkmm (historically "GTK--") is a C++ wrapper for an underlying GTK code base written in C. Compared to Qt, another GUI library, gtkmm uses more modern and native C++ features.
In Ubuntu, installing the development environment is done with the gnome-devel
metapackage:
sudo apt install gnome-devel
NES emulator
Courses
C++ Standard Template Library in Practice
Complete C++ Developer Course
RPGCharGen
#include <iostream>
#include "RPGCharGen.h"
using namespace std;
int main() {
Warrior w{"Doofus McGroober", Race::HUMAN};
cout << "Player name: " << w.getName() << endl;
cout << "Player HP: " << w.getHp() << endl;
cout << "Player MP: " << w.getMp() << endl;
cout << "Player race: " << w.getRace() << endl;
w.attack();
Priest m{"Brother Tolkien", Race::ELF};
cout << "Player name: " << m.getName() << endl;
cout << "Player HP: " << m.getHp() << endl;
cout << "Player MP: " << m.getMp() << endl;
cout << "Player race: " << m.getRace() << endl;
m.attack();
Mage n{"Smart Frodo", Race::DWARF};
cout << "Player name: " << n.getName() << endl;
cout << "Player HP: " << n.getHp() << endl;
cout << "Player MP: " << n.getMp() << endl;
cout << "Player race: " << n.getRace() << endl;
n.attack();
return 0;
}
#if !defined(RPGCHARGEN_H)
#define RPGCHARGEN_H
#include <string>
enum Race { HUMAN, ELF, DWARF };
class Player {
protected:
std::string _name{ "Johnny Bravo" };
Race _race{Race::HUMAN };
int _hp{ 100 };
int _mp{ 100 };
public:
Player(std::string n, Race r, int hp, int mp) : _name{n}, _race{r}, _hp(hp), _mp(mp) {}
virtual std::string attack()= 0;
int getHp() { return _hp; }
int getMp() { return _mp; }
std::string getRace()
{
switch (_race)
{
case 0:
return "human";
break;
case 1:
return "elf";
break;
case 2:
return "dwarf";
break;
default:
return "none";
break;
}
}
std::string getName() { return _name; }
void setHp(int n) { _hp = n; }
void setMp(int n) { _mp = n; }
void setName(std::string s) { _name = s; }
void setRace(Race r) { _race = r;}
};
class Warrior : public Player {
public:
Warrior(std::string n, Race r) : Player(n, r, 200, 0) {}
std::string attack() {return "I will destroy you with my sword, foul demon!";}
};
class Priest : public Player {
public:
Priest(std::string n, Race r) : Player(n, r, 100, 200) {}
std::string attack() {return "Taste the wrath of the Two True Gods!";}
};
class Mage : public Player {
public:
Mage(std::string n, Race r) : Player(n, r, 150, 150) {}
std::string attack() {return "You are overmatched by my esoteric artifices!";}
};
#endif // RPGCHARGEN_H