Table des matières
Modern C++
Le C++ moderne existe depuis C++11, c'est-à-dire depuis 2011.
Certains IDE d'intérêt pour C++:
- CodeLite (gratuit et multi-plateforme)
- CLion (payant et multi-plateforme)
- Windows: Visual Studio, Visual Studio Code
- macOS: XCode
Ressources
Setup Linux
sudo apt install build-essential
Compile by Command-Line Interface
Having a main.cpp
file in current directory:
$ g++ -Wall -std=c++14 main.cpp -o main
-Wall
: All warnings-std
: standard to use-o
output name of executable
Structure d'un programme C++
Preprocessor Directives
#include <iostream> #include "myfile.h" #if #elif #else #endif #ifdef #ifndef #define #line #error #pragma
Comments
// Single line comment /* Multi-line comment */
main() function
Deux versions:
int main() { // code return 0; }
int main(int argc, char *argv[]) { // code return 0; }
argc
est le argument count et argv
sont les paramètres passés par le système d'exploitation.
Namespaces
std
est le nom pour namespacestandard
- Scope resolution operator:
::
Utiliser un namespace:
std::cout << "Some text";
using namespace std; // ... code cout << "Some text";
Qualified using
namespace variant:
using std::cout; // Utiliser seulement ce qui est nécessaire // ... code cout << "Some text";
Variables
VariableType variableName;
Règles:
- On doit déclarer la variable avant de l'utiliser
- Peut contenir des lettres, chiffres, underscores
- Doit commencer par une lettre ou un underscore
- Ne peut pas commencer par un chiffre
- On ne peut utiliser un keyword C++
- On ne peut pas redéclarer un nom de variable déjà déclaré dans le scope
- Les noms de variables sont sensibles à la casse
int age; // uninitialized int age = 21; // C-like initialization int age = (21); // Constructor initialization int age {21}; // C++11 list initialization syntax
Global variables
#include <iostream> int age {18}; // Global int main() { int age {16}; // local, shadowing global one }
Primitive Data Types
Liste non-exhautive (voir Introduction to fundamental data types) :
- short
- int (signed / unsigned)
- long (signed / unsigned)
- char
- bool
Initialization:
char middle_name {'J'}; // single quote for char long people_on_earth = 7'600'000'000; // sera un overflow long long people_on_earth {7'600'000'000}; // ' pour séparer les miliers, depuis C++14 float car_payment {401.23}; long double large_amount {2.7e120}; bool game_over {false}; cout << "Value of game_over is " << game_over << endl; // Value of game_over is 0
sizeof
sizeof(int); int age {31}; sizeof(age); sizeof(long long);
Constantes de minimums et maximums:
- CHAR_MAX / CHAR_MIN
- INT_MAX / IN_MIN
- LONG_MAX
- LLONG_MAX
- etc
Constants
- Constantes litérales
- Declared constants
- Constant expressions
- Enumerated constants
- Defined constants
Literal constants:
12 - integer 12U - unsigned int 12L - long 12LL - long long 12.1 - double 12.1F - float 12.1L - long double
Caracter literal constants : \n
, \r
, etc
Declared constants:
const int age {31}; // Declared constant #define pi 3.1415926 // defined constant, ne pas utiliser
Arrays
Multi-dimentional arrays
int movie_rating [3][4] { {0,3,4,5}, {0,2,5,5}, {0,3,1,5} };
Tableau statique
- Taille fixe d'éléments d'un même type
- La taille est fixé au moment de la compilation et ne peut pas grandir (sauf C99)
- Les éléments sont stockés de façon séquentielle et en continu
- Chaque élément a son propre emplacement
- Accessible via sa position
- Ont un index de base 0
Points importants:
- Les arrays n'est pas un pointeur
<type>[size]
, e.g.int arr[]
estint[5]
- Mais accessible via la syntaxe de pointeurs
- Un array peut être assigné à un tableau
- Le pointeur détient l'adresse du premier élément
- Les autres éléments peuvent être accédés via l'index ou en incrémentant le pointeur
int arr1[5]; // tableau non initialisé int arr2[5]{}; // éléments initialisés avec 0 int arr3[5]{ 1, 2, 3, 4, 5 }; // initialisé avec les valeurs spécifiés
int *p = arr3; *(p + 2) = 800; // Le 3e élément (index 2) aura la valeur 800
Passer un tableau à une fonction
void Print(int *ptr, int size) { for (int i = 0; i < size; ++i) { std::cout << ptr[i] << ' '; //std::cout << *(ptr + i) << ' '; // moins lisible, donc la façon précédente est préférable } } Print(arr3, sizeof(arr3) / sizeof(int));
En tant que référence:
template<typename T, int size> void Print(T(&ref)[size]) { for (int i = 0; i < size; ++i) { std::cout << ref[i] << ' '; } } int(&ref)[5] = arr3; Print(arr);
Vectors
#include <vector> vector <char> vowels; vector <int> test_scores;
Characters and Strings
- C-Style characters
- C++
C-Style Characters
C-Style Strings
- Sequence of characters
- Contiguous in memory
- Implemented as an array of characters
- Terminated by a null character
- null → character with a value of zero
- Referred to as zero or null terminated strings
String Literals
- Sequence of characters in double quotes
- Constant
- Terminated with null character
Fonctions
#include <cctype>
Test Functions | |
---|---|
isalpha(c) | True if c is a letter |
isalnum(c) | True if c is a letter or a digit |
isdigit(c) | True if c is a digit |
islower(c) | True if c is a lowercase letter |
isprint(c) | True if c is a printable character |
ispunct(c) | True if c is a punctuation character |
isupper(c) | True if c is a an uppercase letter |
isspace(c) | True if c is a whitespace |
Conversions | |
---|---|
tolower(c) | |
toupper(c) |
char my_name[] = {"My name"};
char my_name[8]; my_name = "Frank"; // Error strcpy(my_name, "Frank"); // OK
Exemples de fonctions C-Style
#include <cstring> char str[80]; strcpy(str, "Hello "); strcat(str, "there"); // Concatenate cout << strlen(str); //11 strcmp(str, "Another"); // > 0
cstdlib
#include <cstdlib>
Inclus des fonctions pour convertir des chaines C-Style à integer, float, long, etc.
C++ Strings
std::string
is a class in the STL
#include <string>
- std namespace
- contiguous in memory
- dynamic size
- Work with input and output streams
- Lots of useful member functions
- Our familiar operators can be used
- Can be easily converted to C-Style Strings if needed
- Safer
#include <string> using namespace std; string s1; // Empty string s2 {"My name"}; string s3 {s2}; string s4 {"My name", 3}; // "My " string s5 {s3, 0, 2}; // "My" string s6 (3, '-'); // "---" // Concatenation string part1 {"C++"}; string part2 {"is a powerful"}; string sequence = part1 + " " + part2 + " language"; sequence = "C++" + " is powerful"; // Illegal: two C-Style strings can't concatenate this way
Regex
Définir un regex:
std::regex regex(R"(\d{2}));
Functions
Using builtin functions:
#include <iostream> #include <cmath> using namespace std; int main() { double num {}; cout << "Enter number (double): "; cin >> num; cout << "The sqrt of " << num << " is: " << sqrt(num) << endl; return 0; }
#include <iostream> #include <cmath> #include <ctime> using namespace std; int main() { int random_number {}; size_t count {10}; int min {1}; // lower bound (inclusive) int max {6}; // upper bound (inclusive) cout << "RAND_MAX on my system is: " << RAND_MAX << endl; srand(time(nullptr)); for (size_t i {1}; i <= count; ++i) { random_number = rand() % max + min; cout << random_number << endl; } return 0; }
Function definition
- Name
- Same rules as for variables
- Should be meaningful
- Usually a verb or a verb phrase
- Parameter list
- Return type
- Body
int function_name () { statements(s); return 0; } void function_name (int a, std::string b) { statements(s); return; // optional }
Les fonctions doivent être définies avant d'être appelées.
Function Prototypes
Define functions before calling them:
- OK for small programs
- Not a practical solution for larger programs
Use function prototypes:
- Tells the compiler what it needs to know without a full function definition
- Also called forward declarations
- Placed ate the beginning of the program
- Also used in our own header files (.h)
int function_name(); // prototype, no parameters int function_name(int); // prototype int function_name(int a); // we can specify parameter name or not int function_name(int a) { statements(a); return 0; }
Function parameters
Dans la définition d'une fonction, on parle de paramètres, mais quand on appelle la fonction on parle d'arguments.
Quand on passe des données à une fonction, elles sont passées par valeur par défaut.
Formal vs actual parameters:
- Formal parameters: the paramters defined in the function header
- Actual parameters: the paramter used in the function call, the arguments
Default Argument Values
Dans le prototype:
double calc_cost(double base_cost, double tax_rate = 0.06); void print(int = 100);
On peut le faire aussi dans l'implémentation de la fonction, mais pas aux deux endroits. Il est préférable de le faire sur le prototype.
Les valeurs par défaut doivent être placés à la fin de la liste des paramètres.
double calc_cost(double base_cost = 100.0, double tax_rate); // ne fonctionne pas
Overloading Functions
int add_numbers(int, int); double add_numbers(double, double); int main() { cout << add_numbers(10, 20) << endl; cout << add_numbers(10.0, 20.0) << endl; return 0; }
Passing Arrays to Functions
Les tableaux sont passés par référence, donc le changement de valeur dans la fonction a un effet sur les valeurs du tableau externe.
#include <iostream> #include <cmath> using namespace std; void zero_array(int numbers [], size_t size); void print_array(int numbers [], size_t size); int main() { int my_numbers[] = { 1,2,3,4,5 }; zero_array(my_numbers, 5); print_array(my_numbers, 5); return 0; } void zero_array(int numbers [], size_t size) { for (size_t i {0}; i < size; ++i) { numbers[i] = 0; } } void print_array(int numbers [], size_t size) { for (size_t i {0}; i < size; ++i) { cout << numbers[i] << endl; } }
Pour nous aider, selon les cas, on peut définir la fonction avec un paramètre const
:
void print_array(const int numbers [], size_t size) { // ... numbers[0] = 0; // Mènera à une erreur du compilateur }
Pass by reference
void pass_by_ref1(int &num); void pass_by_ref1(int &num) { num = 1000; } int main() { int num {10}; cout << "num before calling pass_by_ref1: " << num << endl; // 10 pass_by_ref1(num); cout << "num after calling pass_by_ref1: " << num << endl; // 1000 return 0; }
Scope rules
- C++ uses scope rules to determine where an identifier can be used.
- C++ uses static or lexical scoping
- Local or Block scope
- Global scope
Local or Block scope:
- Identifiers declared in a block { }
- Fuynction parameters haave block scope
- Only visibke within the block where declared
- Function local var are only active while the fn is executing
- Local varibales are not preserved between function calls
- With nested blocks inner blocks can see but outer blocks cannot see in.
Static local variables:
- Declared with static qualifier
static int value {10};
- Value is preserved between function calls
- Only initialized the first time the function is called
Global scope:
- Identifier declared outside any function or class
- Visible to all parts of the program after the global identifier has been devlared
- Global constant are OK
- Best practice: don't use global variables
using namespace std; void local_example(); void global_example(); void static_local_example(); int num {300}; // Global variable - declared outside any class or function int main() { int num {100}; // Local to main { // creates a new level of scope int num {200}; // local to this inner block } return 0; }
Inline functions
- Function calls have a certain amount of overhead
- We saw what happens on the call stack
- Sometimes we have simple functions
- We can suggest to the compiler to compile them inline
- Avoid function call overhead
- generate inline assembly code
- faster
- could cause code bloat
- Compilers optimizations are very sophisticated
- Will likely inline even witout any explicit suggestion
inline int add_numbers(int a, int b) { // definition return a + b; } int main() { int result {0}; result = add_numbers(100, 200); // call return 0; }
Recursive Functions
A recursive function is a function that calls itself, either directly or indirectly through another function.
#include <iostream> using namespace std; unsigned long long factorial(unsigned long long); unsigned long long factorial(unsigned long long n) { if (n == 0) { return 1; // base case } return n * factorial(n-1); // recursive case } int main() { cout << factorial(3) << endl; // 6 return 0; }
#include <iostream> using namespace std; unsigned long long fibonacci(unsigned long long n); unsigned long long fibonacci(unsigned long long n) { if (n <= 1) { return n; // base cases } return fibonacci(n-1) + fibonacci(n-2); // recursion } int main() { cout << fibonacci(5) << endl; // 5 return 0; }
Function Pointers
float Add(float x, float y) { return x + y; } float Substract(float x, float y) { return x - y; } int main() { float(*FnPtr)(float, float); // Declaration of function pointer FnPtr FnPtr = Add; // or '&Add', both works float result = FnPtr(3.1f, 8.3f); std::cout << result << std::endl; // We can reassign FnPtr, provided that function referenced has same signature FnPtr = &Substract; // Using amphersand here, more verbose, but again, same as without it }
Another example:
const char* GetErrorMessage(int errorNo) { switch (errorNo) { case 0: return "Error 0"; case 1: return "Error 1"; case 2: return "Error 2"; default: return "Unknown error" } } int main() { const char* (*Pfn)(int) = GetErrorMessage; std::cout << Pfn(1) << std::endl; }
Function pointers as Arguments
using PFN = float(*)(float, float); float Operation(float x, float y, PFN pfn) { if (pfn == nullptr) { std::cout << "Invalid operation" << std::endl; return 0; } float result = pfn(x, y); return result; } int main() { Operation(1, 2, Add); }
Instead of declaring a function pointer type, we can do inline in function, but declaring a type might be preferable:
float Operation(float x, float, y, float(*pfn)(float, float)) { // implementation }
Two ways of declaring a function pointer type:
typedef float(*PFN)(float, float); float Operation(float x, float y, PFN) { ... }
or
using PFN = typedef float(*)(float, float); float Operation(float x, float y, PFN pfn) { ... }
Pointers and References
Qu'est-ce qu'un pointeur:
- C'est une variable dont la valeur est une adresse mémoire
Qu'est-ce qui peut être à cette adresse ?
- Une autre variable
- une fonction
Pour utiliser la valeur dont le pointeur pointe, on doit connaitre son type.
Pourquoi utiliser des pointeurs ?
- Opérations sur des tableaux plus efficacement
- On peut allouer de la mémoire dynamiquement dans le heap (ou le free store)
- Cette mémoire n'a pas de nom de variable
- La seule façon d'y accéder est par un pointeur
- Avec l'OO le polymorphisme fonctionne grâce aux pointeurs
- On peut accéder à des adresses mémoire spécifiques.
Déclarer des pointeurs
variable_type *pointer_name; // syntaxe générale int *int_ptr; double* double_ptr; char *char_ptr; string *string_ptr;
Les deux possibilités de l'astérisque sur le type ou sur le nom sont acceptés. Généralement on le met sur le nom de variable/pointer.
Initialiser des pointeurs qui pointent nul part.
- Toujours initialiser les pointeurs
- Les pointeurs non initialisés contiennent des données invalidées (garbage data)
- Initialiser un pointeur à zero ou sur
nullptr
(C++11) représente l'adresse zero.- Implique que le pointeur pointe nul part
int *int_ptr {}; int *int_ptr {nullptr};
Endianess
- Little endian: Le dernier byte d'une adresse est stocké en premier (c'est le cas avec Windows)
- Big endian: Le premier byte d'une adresse est stocké en premier
Illustration du Little Endian:
Ici on voit que l'adresse du pointeur ptr
est 0x001FFCC0
et sa valeur est l'adresse de data
, c'est-à-dire 0x001FFCCC
. Mais dans la mémoire, si on inspecte la valeur à cet endroit, on verra CC FC 1F 00
.
Accéder/Stocker à l'adresse du pointeur
Il faut utiliser l'opérateur adresse (address operator): &
int num{10}; cout << "Value of num is: " << num << endl; // 10 cout << "sizeof of num is: " << sizeof num << endl; // 4 cout << "Address of num is: " << &num << endl; // 0x7fffd0421194 (garbage)
sizeof
d'un pointeur
- Ne pas confondre la taille de la variable pointeur au type du pointeur
- Tous les pointeurs dans un programme on la même taille
- Ils peuvent pointer sur des types vraiment gros ou vraiment petits
int *p; cout << "\nValue of p is: " << p << endl; // 0x7f8ccaa45048 (garbage) cout << "Address of p is: " << &p << endl; // 0x7ffd39db3f70 cout << "sizeof of p is: " << sizeof p<< endl; // 8 p = nullptr; cout << "\nValue of p is: " << p << endl; // 0
Stocker une adresse dans un pointeur
Le compilateur va s'assurer que l'adresse stockée dans un pointer est du bon type.
int score{10}; double high_temp{100.7}; int *score_ptr {nullptr}; score_ptr = &score; cout << "Value of score is: " << score << endl; cout << "Address of score is: " << &score << endl; cout << "Value of score_ptr is: " << score_ptr << endl; score_ptr = &high_temp; // Compiler error
String
string s1 {"Some string"}; string *p1 {&s1};
Déréférencement d'un pointeur
Accéder à la valeur où le pointeur pointe → deferencing a pointer
Par exemple, si un pointeur nommé score_ptr
a une adresse valide, on peut donc accéder à sa valeur à l'adresse contenue dans score_ptr
en utilisant l'opérateur de déférencement: *
.
int score {100}; int *score_ptr {&score}; cout << *score_ptr << endl; // 100 *score_ptr = 200; cout << *score_ptr << endl; // 200 cout << score << endl; // 200
Dynamic Memory Allocation
Allocating storage from heap at runtime.
- We don't often know how much storage we need until we need it
- We van allocate storage for a variable at runtime
- Recall C++ arrays
- We had to explicitly provide the size and it was fixed
- But vectors grow and shrink dynamically
- We can use pointers to access newly allocated heap storage
int *int_ptr {nullptr}; int_ptr = new int; // allocate the int on the heap cout << int_ptr << endl; // use it delete int_ptr; // release it
Utilisation de new []
:
int *array_ptr {nullptr}; size_t size{0}; cout << "How big do you want the array? "; cin >> size; array_ptr = new int[size]; // ... delete [] array_ptr;
Lien entre un tableau et un pointeur
int scores[] {100, 95, 89}; int *score_ptr {scores};
Pointer subscript notation
cout << score_ptr[0] << endl; // 100 cout << score_ptr[1] << endl; // 95 cout << score_ptr[2] << endl; // 89
Pointer offset notation
cout << *score_ptr << endl; // donne 100, mais exemple addr est 0x73f610 cout << *(score_ptr + 1) << endl; // donne 95, addr est 0x73f614 (ajoute 4 car 4 est le size d'un int cout << *(score_ptr + 2) << endl; // donne 89, addr est 0x73f618
Arithmétique des pointeurs
++
: incrémente le pointer pour pointer vers le prochain élément (du tableau)int_ptr++
--
: décrémente le pointer pour pointer vers l'élément précédent (du tableau)int_ptr--
int scores[] {100, 95, 89, 68, -1}; // -1 est une valeur sentinelle int *score_ptr {scores}; while (*score_ptr != -1) { cout << *score_ptr << endl; score_ptr++; } // Ou: while (*score_ptr != -1) { cout << *score_ptr++ << endl; }
Constant pointers
On ne peut pas changer la valeur (l'adresse) du pointeur constant.
int high_score {100}; int low_score {65}; int *const score_ptr {&high_score}; *score_ptr = 86; // OK score_ptr = &low_score; // Error
Passing pointers to a function
void double_data(int *int_ptr) { *int_ptr *= 2; } int main() { int value {10}; int *int_ptr {nullptr}; cout << "Value: " << value << endl; // 10 double_data(&value); cout << "Value: " << value << endl; // 20 int_ptr = &value; double_data(int_ptr); cout << "Value: " << value << endl; // 40 }
voi display(vector<string> *v) { (*v).at(0) = "Funny"; }
Returning a pointer from a Function
int *create_array(size_t size, int init_value = 0) { int *new_storage {nullptr}; new_storage = new int[size]; for (size_t i{0}; i < size; ++i) *(new_storage + i) = init_value; return new_storage; }
Pitfalls
- Uninitialized pointers
- Dangling pointers
- Not checking if new failed to allocate memory
- Leaking memory
Reference
Qu'est-ce qu'une référence:
- An alias for a variable
- Must be initialiazed to a variable when declared
- Cannot be null
- Once initialized, cannot be made to refer ro a different variable
- Very useful as function parameters
- Might be helpful to think of a reference as a constant pointer that is automatically deferenced
int num {100}; int &ref {num}; vector<string> stooges {"Larry", "Moe", "Curly"}; for (auto str: stooges) str = "Funny"; // str is a COPY of the each vector element for (auto str:stooges) // No change cout << str << endl; for (auto &str: stooges) // str is a reference to each vector element str = "Funny"; for (auto const &str:stooges) // notice we are using const cout << str << endl; // now the vector elements have changed
L-values and R-values
L-value:
- Values that have names and are addressable
- Modifiable if they are not constants
int x {100}; // x is an l-value string name; // name is an l-value 100 = x; // 100 is not a l-value
R-Values can be assigned to l-values explicitly
int x {100}; // 100 is a r-value int y = x + 200 // (x+200) is a r-value
Quand utiliser un pointeur vs une référence
- Pass-by-value
- When the function does not momdify the actual parameter, and
- the parameter is small and efficient to copy like simple types (int, char, double, etc)
- Pass-by-reference using a pointer
- When the function does modify the actual parameter, and
- the parameter is expensive to copy and
- It's OK to the pointer to contain nullptr value
- Pass-by-reference using a pointer to
const
- When the function does not modify the actual parameter, and
- the parameter is expensive to copy and
- It's OK to the pointer to contain
nullptr
value
- Pass-by-reference using a
const
pointer toconst
- When the function does not modify the actual parameter, and
- the parameter is expensive to copy and
- It's OK to the pointer to contain
nullptr
value - We don't want to modify the pointer itself
- Pass-by-reference using a reference
- When the function does modify the actual parameter, and
- the parameter is expensive to copy and
- The parameter will never be
nullptr
- Pass-by-reference using a
const
reference- When the function does not modify the actual parameter, and
- the parameter is expensive to copy and
- The parameter will never be
nullptr
OOP - Classes et objets
Definition
class Account { std::string name; double balance; bool withdraw(double amount); bool deposit(double amount); };
Création d'objets
Account account1; Account account2; Account accounts[] { account1, account2 }; std::vector<Account> accounts { account1 }; accounts.push_back(account2);
Access Class Members
If we have an object (dot operator)
Account account1; account1.balance; account1.deposit(100.0);
If we have a pointer to an object (member of a pointer operator)
- Deference thhe pointer then use the dot operator
Account *account1 = new Account(); (*account1)->balance; (*account1)->deposit(100.0);
- Or use the member of pointer operator (arrow operator)
Account *account1 = new Account(); account1->balance; account1->deposit(100.0);
Access modifiers
class Player { private: std::string name; int health; int xp; public: void talk(std::string text_to_say); bool is_dead(); };
Implementing Member methods
- Very similar to how we implement functions
- Member methods have access to member attributes
- So you don't need to pass them as arguments!
- Can be implemented inside the class declaration
- Implicitly inline
- Can be implemented outside the class declaration
- Need to use
Class_name::method_name
- Can separate specification from implementation
- .h file for class declaration
- .cpp file for the class implementation
Account.h
class Account { private: // attributes std::string name; double balance; public: // methods // declared inline void set_balance(double bal) { balance = bal; } double get_balance() { return balance; } // methods will be declared outside the class declaration void set_name(std::string n); std::string get_name(); bool deposit(double amount); bool withdraw(double amount); };
Account.cpp
#include "Account.h" void Account::set_name(std::string n) { name = n; } std::string Account::get_name() { return name; } bool Account::deposit(double amount) { // if verify amount balance += amount; return true; } bool Account::withdraw(double amount) { if (balance-amount >= 0) { balance -= amount; return true; } else { return false; } }
main.cpp
// Section 13 // Implementing member methods 2 #include <iostream> #include "Account.h" using namespace std; int main() { Account frank_account; frank_account.set_name("Frank's account"); frank_account.set_balance(1000.0); if (frank_account.deposit(200.0)) { cout << "Deposit OK" << endl; } else { cout << "Deposit Not allowed" << endl; } return 0; }
Include guard
#ifndef _ACCOUNT_H_ #define _ACCOUNT_H_ // Account class declaration #endif
Ou
#pragma once
Constructeurs et Destructeurs
Constructors:
- Special member method
- Invoked during object creation
- Useful for initialization
- Same name as the class
- No return type is specified
- Can be overloaded
Destructors:
- Special member method
- Same name as the class proceeded with a tilde (
~
) - Invoked automatically when an object is destroyed
- No return type and no parameters
- Only one desctructor is allowed per class - cannot be overloaded
- Useful to release memory and other ressources
class Player { private: std::string name; int health; int xp; public: void set_name(std::string name_val) { name = name_val; } // Overloaded Constructors Player() { cout << "No args constructor called"<< endl; } Player(std::string name) { cout << "String arg constructor called"<< endl; } Player(std::string name, int health, int xp) { cout << "Three args constructor called"<< endl; } ~Player() { cout << "Destructor called for " << name << endl; } };
Creating objects
{ Player slayer; Player frank {"Frank", 100, 4}; Player hero {"Hero"}; // use the objects } // 4 destructors called Player *enemy = new Player("Enemy", 100, 0); delete enemy; // destructor called
If we don't provide constructor and/or destructor, C++ will automatically provide default constructor/destructor that are empty.
Default constructor
- Does not expect any arguments
- Also called the no-args constructor
- If we write no constructors at all for a class, C++ will generate a Default Constructor that does nothing
- Called when you instantiate a new object with no arguments
Player frank; Player *enemy = new Player;
If implemented
// Section 13 // Default Constructors #include <iostream> #include <string> using namespace std; class Player { private: std::string name; int health; int xp; public: void set_name(std::string name_val) { name = name_val; } std::string get_name() { return name; } Player() { name = "None"; health = 100; xp = 3; } }; int main() { Player hero; // this is calling the default constructor, initializing name, health and xp return 0; }
Overloading Contructor
- Classes can have as many constructors as necessary
- Each must have a unique signature
- Default constructor is no longer compiler-generated once another constructor is declared
Constructor initialization lists
- So far, all data member values have been set in the constructor body
- Constructor initialization lists
- are more efficient
- initialization list immediately follows the parameter list
- initializes the data members as the object is created
- order of initialization is the order of declaration in the class
class Player { private: std::string name {"XXXXXXX"}; int health; int xp; public: // Overloaded Constructors Player(); Player(std::string name_val); Player(std::string name_val, int health_val, int xp_val); }; Player::Player() : name{"None"}, health{0}, xp{0} { } Player::Player(std::string name_val) : name{name_val}, health{0}, xp{0} { } Player::Player(std::string name_val, int health_val, int xp_val) : name{name_val}, health{health_val}, xp{xp_val} { // empty body, but code can added obviouly } int main() { Player empty; Player frank {"Frank"}; Player villain {"Villain", 100, 55}; return 0; }
Delegating Constructors
- Often the code for constructors is very similar
- Duplicated code can lead to errors
- C++ allows delegating constructors
- Code for one constructor can call another in the initialization list
- avoids duplicating code
#include <iostream> #include <string> using namespace std; class Player { private: std::string name; int health; int xp; public: // Overloaded Constructors Player(); Player(std::string name_val); Player(std::string name_val, int health_val, int xp_val); }; Player::Player() : Player {"None", 0, 0} { cout << "No-args constructor" << endl; } Player::Player(std::string name_val) : Player {name_val, 0, 0} { cout << "One-arg constructor" << endl; } Player::Player(std::string name_val, int health_val, int xp_val) : name{name_val}, health{health_val}, xp{xp_val} { cout << "Three-args constructor" << endl; } int main() { Player empty; Player frank {"Frank"}; Player villain {"Villain", 100, 55}; return 0; }
Default Constructor Parameters
- Can often simplify our code and reduce the number of overloaded constructors
- Same rules apply with non-member functions
#include <iostream> #include <string> using namespace std; class Player { private: std::string name; int health; int xp; public: Player(std::string name_val = "None", int health_val = 0, int xp_val = 0); // Player() {} // Will cause a compiler error }; Player::Player(std::string name_val, int health_val, int xp_val) : name{name_val}, health{health_val}, xp{xp_val} { cout << "Three-args constructor" << endl; } int main() { Player empty; Player frank {"Frank"}; Player hero {"Hero", 100}; Player villain {"Villain", 100, 55}; return 0; }
Copy Constructor
- When objects are copied, C++ must create a new object from an existing object
- When is a copy of an object?
- Passing object value as a parameter
- Returning an object from a function by value
- Constructing one object based on another of the same class
- C++ must have a way of accomplishing this so it provides a compiler-defined copy constructor if you don't
Pass object by-value
Player hero {"Hero", 100, 20}; void display_player(Player p) { // p is a COPY of hero in this example // ... use p // Desctructor for p will be called } display_player(hero);
Return object by value
Player enemy; Player create_super_enemy() { Player an_enemy {"Super Enemy", 1000, 1000}; return an_enemy; // A copy of 'an_enemy' is returned. } enemy = create_super_enemy();
Construct one object based on another
Player hero {"Hero", 100, 100}; Player another_hero {hero}; // A copy of 'hero' is made
Default Copy Constructor
- If you don't provide your own way of copying objects by value, then the compiler provides a default way of copying objects
- Copies the values of each data member to the new object
- Default memberwise copy
- Perfectly fine in many cases
- Beware if you have a pointer data member
- Pointer will be copied
- Not what it is pointing to
- Shallow vs Deep copy
Best pratices
- Provide a copy constructor when your class has raw pointer members
- Provide a copy constructor with a const reference parameter
- Use STL classes as the already provide copy constructors
- Avoid using raw pointer data members if possible
Declaring the Copy Constructor
Type::Type(const Type &source); Player::Player(const Player &player);
Implementation:
Player::Player(const Player &source) : name{source.name}, health {source.health}, xp {source.xp} { }
Shallow Copy
Default Copy Constructor
- Memberwise copy
- Each data member is copied from the source object
- The pointer is copied, but not what it points to (shallow copy)
- Problem: when we release the storage in the destructor, the other object still refers to the released storage.
Deep Copy
Deep copy: create new storage and copy values.
Deep::Deep(const Deep &source) { data = new int; // allocate storage *data = *source.data; }
Deep copy constructor - delegating constructor
Deep::Deep(const Deep &source) : Deep {*source.data} { cout << "Copy constructor - deep" << endl; }
Move Constructor
- Sometimes when we execute code, the compiler creates unnamed temporary values
int total {0}; total = 100 + 200;
100 + 200
is evaluated and300
stored in an unnamed temp value- The
300
is then stored in the variabletotal
- Then the temp value is discarded
- The same happens with objects as well
When is it useful?
- Sometimes copy constructors are called many times automatically due to the copy semantics of C++
- Copy constructors doing deep copying can have a significant performance bottleneck
- C++11 introduced move sementics and the move constructor
- Move constructor moves an object rather than copy it
- Optional but recommended when you have a raw pointer
- Copy elision - C++ may optimize copying away completely (RVO → Return Value Optimization)
R-Values:
- Used in moving semantics and perfect forwarding
- Move semantics is all about r-value references
- Used by move constructor and move assignment operator to efficiently move an object rather than copy it
- R-Value reference operator:
&&
#include <iostream> #include <vector> using namespace std; class Move { private: int *data; public: void set_data_value(int d) { *data = d; } int get_data_value() { return *data; } Move(int d); // Constructor Move(const Move &source); // Copy Constructor Move(Move &&source) noexcept; // Move Constructor ~Move(); // Destructor }; Move::Move(int d) { data = new int; *data = d; cout << "Constructor for: " << d << endl; } // Copy ctor Move::Move(const Move &source) : Move {*source.data} { cout << "Copy constructor - deep copy for: " << *data << endl; } //Move ctor Move::Move(Move &&source) noexcept : data {source.data} { source.data = nullptr; cout << "Move constructor - moving resource: " << *data << endl; } Move::~Move() { if (data != nullptr) { cout << "Destructor freeing data for: " << *data << endl; } else { cout << "Destructor freeing data for nullptr" << endl; } delete data; } int main() { vector<Move> vec; vec.push_back(Move{10}); vec.push_back(Move{20}); // .. vec.push_back(Move{80}); return 0; }
The this pointer
this
is a reserved keyword- Contains the address of the object - so it's a pointer to the object
- Can only be used in class scope
- All member access is done via the
this
pointer - Can be used by the programmer
- to access data member and methods
- to determine if two objects are the same
- can be deferenced (
*this
) to yield the current object
Const with classes
const Player villain {"Villain", 100, 55};
comme paramètre:
void display_player_name(const Player &p) { cout << p.get_name() << endl; } display_player_name(villain); // ERROR : compiler asumes that get can modify object/name
Const methods:
class Player { private: // ... public: std::string get_name() const; // ... };
Static class members
- Class data members can be declared as static
- A single data member that belongs to the class, not the objects
- Useful to store class-wide information
- Class function can be declared as static
- Independent of any objects
- Can be called using the class name
class Player { private: static int num_players; public: static int get_num_players(); }; int Player::num_players = 0;
Static method has only access to static members:
int Player::get_num_players() { return num_players; }
Struct vs Classes
- In addition to define a
class
we can declare astruct
struct
comes from the C programming language- Essentially the same as a
class
expect- Members are
public
by default (in classes, members are private by default)
class Person { std::string name; std::string get_name(); // Why if name is public? }; Person p; p.name = "Joe"; // Compiler error - private cout << p.get_name(); // Compiler error - private
struct Person { std::string name; std::string get_name(); // Why if name is public? } Person p; p.name = "Joe"; // OK, public cout << p.get_name(); // OK, public
Another example of struct:
struct Person { std::string name; int age; } Person p1 {"Curly", 31}; Person p2 {"Moe", 39};
Friends of a class
- Friend
- A function or a class that has access to private class member
- And, that function of a class is NOT a member of the class it is accessing
- Function
- Can be regular non-member functions
- Can be member methods of another class
- Class
- Another class can have access to private class members
Considerations:
- Friendship must be granted, NOT taken
- Declared explicitly in the class that is granting friendship
- Declared in the function prototype with the keyword
friend
- Friendship is not symmetric
- If A is a friend of B, B is NOT necessary a friend of A (again, must be granted)
- Friendship is not transitive
- If A is a friend of B, AND
- B is a friend of C
- then A is NOT a friend of C
class Player { friend void display_player(Player &p); std::string name; int health; int xp; public: // ... };
void display_player(Player &p) { std::cout << p.name << std::endl; // can change private members std::cout << p.health << std::endl; std::cout << p.xp << std::endl; }
Member function of another class:
class Player { friend void Other_class::display_player(Player &p); std::string name; int health; int xp; public: // ... };
class Other_class { ... public: void display_player(Player &p) { std::cout << p.name << std::endl; // ... } };
Another class as a friend
class Player { friend class Other_class; };
Operator Overloading
What is Operator Overloading?
- Using traditional operators such as +, =, * etc with user-defined types
- Allows user defined types tp behave similar to build-in types
- Can make code more readable and writable
- Not done automatically (except for assignment operator), they must be explicitly defined
See Introduction to operator overloading for more details.
Copy assignment operator
- C++ provides a default asignment operator used for assigning one object to another.
Mystring s1 {"Joe"}; Mystring s2 = s1; // NOT assignment, same as Mystring s2{s1}; s2 = s1; // assignment
- Default is memberwise assignment (shallow copy), if we have a raw pointer data memeber, we must deep copy
Syntax:
Type &Type::operator=(const Type &rhs);
Example:
Mystring &Mystring::operator=(const Mystring &rhs); s2 = s1; // we write this s2.operator=(s1); // operator= method is called
Implementation:
Mystring &Mystring::operator=(const Mystring &rhs) { if (this == &rhs) { return *this; } delete [] str; str = new char[std::strlen(rds.str + 1]; std::strcopy(str, rhs.str); return *this; }
Move Assignment Operator (=)
- You cna choose to overload the move assignment operator
- C++ will use the copy assignment operator if necessary
Mystring s1; s1 = Mystring {"Joe"}; // Move assignment
- If we have raw pointer we should overload the move assignment operator for efficiency
Type &Type::operator=(Type &&rhs);
Instead of copying data to a new memory location, we steal the pointer of the source (RHS).
Declaration in .h file:
Mystring(Mystring &&rhs); // move constructor, old version, not present, only for demonstration Mystring &operator=(Mystring &&rhs); // move constructor, overloaded
Implementation:
Mystring &Mystring::operator=(Mystring &&rhs) { std::cout << Using move assignment" << std::endl; if (this == && &rhs) { return *this; } delete [] str; str = rhs.str; rhs.str = nullptr; return *this; }
Unary operator as member methods (++, --, -, !)
Syntax:
ReturnType Type::operatorOp();
Examples:
Number Number::operator-() const; Number Number::operator++() const; // pre-increment Number Number::operator++(int) const; // post-increment bool Number::operator!() const;
Inheritance
What is it and why is it used?
- Provides a method for creating new classes from existing classes
- The new class contains the data and behaviours of the existing class
- Allows for reuse of existing classes
- Allows us to focus on the common attributes among a set of classes
- Allows new classes to modify behaviors of existing classes to make it unique
- Without actually modifying the original class
class Account { // balance, deposit, withdraw }; class Savings : public Account { // interest rate, specialized withdraw };
Terminology
- Inheritance
- Process of creating new classes from existing classes
- Reuse mechanism
- Single inheritance
- A new class is created from another 'single' class
- Multiple inheritance
- A new class is created from two (or more) other classes
- Base class (parent class, super class): The class being extended or inherited from
- Derived class (child class, sub class):
- The class being created from Base class
- Will inherit attributes and operations from Base class
- “Is-A” relationship
- Public inheritance
- Derived classes are sub-types of their Base classes
- Can use a derived class object whereever we use a base class object
- Generalization: Combining similar classes into single, more general class based on common attributes
- Specialization: Creating new classes from existing classes proving more specialized attributes or operations
- Inheritance of Class Hierachies
- Organization of our inheritance relationships
C++ Derivation Syntax
class Base { // Base class members }; class Derived: access-specifier Base { // Derived class members };
Constructors and Destructors
- A derived class inherits from its Base class
- The Base part of the Derived class must be initialized before the Derived class is initialized
- When a derived object is created:
- Base class constructor executes
- then Derived class contructor executes
Passing Arguments to base constructors
class Base { public: Base(); Base(int); // ... }; Derived::Derived(int x) : Base(x) { // optional initializers for Derived // code };
Complete implementation:
#include <iostream> using namespace std; class Base { private: int value; public: Base() : value {0} { cout << "Base no-args constructor" << endl; } Base(int x) : value {x} { cout << "Base (int) overloaded constructor" << endl; } ~Base() { cout << "Base destructor" << endl; } }; class Derived : public Base { private: int doubled_value; public: Derived() : Base {}, doubled_value {0} { cout << "Derived no-args constructor " << endl; } Derived(int x) : Base{x}, doubled_value {x * 2} { cout << "Derived (int) constructor" << endl; } ~Derived() { cout << "Derived destructor " << endl; } }; int main() { // Derived d; Derived d {1000}; return 0; }
Copy constructor
- Can invoke Base copy constructor explicitly
- Derived object
other
will be sliced.
Derived(const Derived &other) : Base(other), doubled_value {other.doubled_value} { cout << "Derived copy constructor" << endl; }
Redefining Base Class Methods
- Derived class can directly invoke Base class methods
- Derived class can override or redefine Base class methods
- Very powerful in the context if polymorphism
class Account { public: void deposit(double amount) { balance += amount; } }; class Savings_Account: public Account { public: void Savings_Account::deposit(double amount) { // Redefine Base class method amount = amount + (amount * int_rate/100); Account::deposit(amount); // invoke call Base class method } };
Static Binding of method calls
Base b; b.deposit(10.0); // Base::deposit Derived d; d.deposit(10.0); // Derived::deposit Base *.ptr = new Derived(); ptr->deposit(10.0); // Base::deposit
Multiple Inheritance
- A derived class inherits from two or more Base classes at the same time
- The Base classes may belong to unrelated class hierarchies
- A Department Chair:
- Is-A Faculty and
- Is-A Administrator
class Department_Chair : public Faculty, public Administrator { // code };
- Some compelling use-cases
- Easily misused
- Can be very complex
Polymorphism
- Fundamental to Object-Oriented programming
- Polymorphism
- Compile-time / early binding / static binding (all refers to same concept)
- Runtime / late binding / dynamic binding
- Runtime Polymorphism
- Being able to assign different meanings to the same function at runtime
- Allows us to program more abstractly
- Think general vs specific
- Let C++ figure out which function to call at runtime
- Not the default in C++, runtime polymorphism is acheived via
- Inheritence
- Base class pointers or references
- virtual functions
An non-polymorphic example - Static Binding
Account a; a.withdraw(1000); // Account::withdraw() Savings b; b.withdraw(1000); // Savings::withdraw() Checking c; c.withdraw(1000); // Checking::withdraw() Trust d; d.withdraw(1000); // Trust::withdraw() Account *p = new Trust(); p->withdraw(1000); // Account::withdraw // should be // Trust::withdraw()
An polymorphic example - Dynamic Binding
withdraw
method is virtual in Account
Account a; a.withdraw(1000); // Account::withdraw() Savings b; b.withdraw(1000); // Savings::withdraw() Checking c; c.withdraw(1000); // Checking::withdraw() Trust d; d.withdraw(1000); // Trust::withdraw() Account *p = new Trust(); p->withdraw(1000); // Trust::withdraw
display
method is virtual in Account
Below, the display_account()
will always call the display
method based on the object's type at runtime.
Code example
Example using static binding:
#include <iostream> class Base { public: void say_hello() const { std::cout << "Hello - I'm a Base class object" << std::endl; } }; class Derived: public Base { public: void say_hello() const { std::cout << "Hello - I'm a Derived class object" << std::endl; } }; void greetings(const Base &obj) { std::cout << "Greetings: "; obj.say_hello(); } int main() { Base b; b.say_hello(); // "Hello - I'm a Base class object" Derived d; d.say_hello(); // "Hello - I'm a Base class object" greetings(b); // "Greetings: Hello - I'm a Base class object" greetings(d); // "Greetings: Hello - I'm a Base class object" Base *ptr = new Derived(); ptr->say_hello(); // "Hello - I'm a Base class object" std::unique_ptr<Base> ptr1 = std::make_unique<Derived>(); ptr1->say_hello(); // "Hello - I'm a Base class object" delete ptr; return 0; }
Using a Base class pointer
Account *p1 = new Account(); Account *p2 = new Savings(); Account *p3 = new Checking(); Account *p4 = new Trust(); vector<Account *> accounts {p1, p2, p3, p4}; for (auto acc_ptr: accounts) { acc_ptr->withdraw(1000); } // delete pointers
#include <iostream> #include <vector> class Account { public: virtual void withdraw(double amount) { std::cout << "In Account::withdraw" << std::endl; } virtual ~Account() { } }; class Checking: public Account { public: virtual void withdraw(double amount) { std::cout << "In Checking::withdraw" << std::endl; } virtual ~Checking() { } }; class Savings: public Account { public: virtual void withdraw(double amount) { std::cout << "In Savings::withdraw" << std::endl; } virtual ~Savings() { } }; class Trust: public Account { public: virtual void withdraw(double amount) { std::cout << "In Trust::withdraw" << std::endl; } virtual ~Trust() { } }; int main() { std::cout << "\n === Pointers ==== " << std::endl; Account *p1 = new Account(); Account *p2 = new Savings(); Account *p3 = new Checking(); Account *p4 = new Trust(); p1->withdraw(1000); p2->withdraw(1000); p3->withdraw(1000); p4->withdraw(1000); std::cout << "\n === Array ==== " << std::endl; Account *array [] = {p1, p2, p3, p4}; for (auto i=0; i<4; ++i) { array[i]->withdraw(1000); } std::cout << "\n === Array ==== " << std::endl; array[0] = p4; for (auto i=0; i<4; ++i) { array[i]->withdraw(1000); } std::cout << "\n === Vector ==== " << std::endl; std::vector<Account *> accounts {p1, p2, p3, p4}; for (auto acc_ptr: accounts) { acc_ptr->withdraw(1000); } std::cout << "\n === Clean up ==== " << std::endl; delete p1; delete p2; delete p3; delete p4; return 0; }
Virtual Functions
- Redefined functions are bound statically
- Overriden functions are bound dynamically
- Virtual functions are overriden
- Allow us to treat all objects generally as objects of the Base class
Declaring virtual functions:
- Declare the function you want to override as virtual in the Base class
- Virtual functions are virtual all the way down the hierarchy from this point
- Dynamic polymorphism only via Account class pointer or reference
class Account { public: virtual void withdraw(double amount); // ... };
Declaring virtual functions (derived classes):
- Override the function in the Derived class
- Function signature and return type must match exactly
- Virtual keyword not required but is best practice
- If you don't provide overriden version it is inherited from it's base class
class Checking: public Account { public: virtual void withdraw(double amount); // ... };
Virtual Destructors
- Problems can happen when we destroy polymorphic objects
- If a derived class is destroyed by deleting its storage via the base class pointer and the class a non-virtual destructor, then the behavior is undefined in the C++ standard.
- Derived objects must be destroyed in the correct order starting ate the correct destructor
Solution / Rule:
- If a class has a virtual function, always provide a public virtual destructor
- If base class destructor is virtual, then all derived class destructors are also virtual
class Account { public: virtual void withdraw(double amount); virtual ~Account(); // ... }; class Savings: public Account { public: virtual void withdraw(double amount); virtual ~Savings() { } };
Using the Override Specifier
- We can override Base class virtual functions
- The function signature and return must be exactly the same
- If they are different, then we have redefinition not overriding
- Redefinition is statically bound
- Overriding is dynamically bound
- C++11 provides an override specifier to have the compiler ensure overriding
Illustrated problem:
#include <iostream> class Base { public: virtual void say_hello() const { std::cout << "Hello - I'm a Base class object" << std::endl; } virtual ~Base() {} }; class Derived: public Base { public: virtual void say_hello() { // Notice I forgot the const std::cout << "Hello - I'm a Derived class object" << std::endl; } virtual ~Derived() {} }; int main() { Base *p1 = new Base(); p1->say_hello(); Derived *p2 = new Derived(); p2->say_hello(); Base *p3 = new Derived(); p3->say_hello(); return 0; }
say_hello
method signatures are different- So Derived redefines
say_hello
instead of overriding it
In the output, third line should say Hello - I'm a Derived class object
:
Hello - I'm a Base class object Hello - I'm a Derived class object Hello - I'm a Base class object
Correction:
class Derived: public Base { public: virtual void say_hello() const override { std::cout << "Hello - I'm a Derived class object" << std::endl; } virtual ~Derived() {} };
The final specifier
C++11 provides the final specifier
- When used at the class level, it prevents a class from being derived from
- When used at the method level, it prevents virtual method from being overriden in derived classes
Syntax:
class MyClass final { // ... };
Syntax on derived class:
class Derived final: public Base { // .. }
Method level
class A { public: virtual void do_something(); }; class B: public A { public: virtual void do_something() final; }; class C: public B { public: virtual void do_something(); // Compiler error: Can't override };
Using Base class references
- We can also use Base class references with dynamic polymorphism
- Useful when we pass objects to functions that expects a Base class reference
Account a; Account &ref = a; ref.withdraw(100); // Account::withdraw Trust t; Account &ref1 = t; ref1.withdraw(100); // Trust::withdraw
void do_withdraw(Account &account, double amount) { account.withdraw(amount); } Account a; do_withdraw(a, 100); // Account::withdraw Trust t; do_withdraw(t, 100); // Trust::withdraw
Pure Virtual Functions and Abstract Classes
Abstract class:
- Cannot instantiate objects
- These classes are used as base classes in inheritance hierarchies
- Often referred to as Abstract Base Classes
Concrete class:
- Used to instantiate objects from
- All their member functions are defined
Abstract Base Class:
- Too generic to create objects from
- Serves as parent to Derived classes that may have objects
- Contains at least one pure virtual function
Pure virtual function:
- Used to make a class abstract
- Specified with
=0
in its declarationvirtual void function() = 0;
- Typically do not provide implementation
- Derived classes must override the base class
- If the Derived class does not override, then it is also abstract
- Used when it doesn't make sense for a base class to have an implementation
- But concrete classes must implement it.
- Example with
Shape
:virtual void draw() = 0;
Abstract Classes as Interfaces
- An abstract class taht has only pure functions
- These functions provide a general set of services to the user of the class
- Provided as public
- Each subclass is free to implement these services as needed
- Every service (method) must be implemented
- The service type information is strictly enforced
Printable example:
- C++ does not provide true interfaces
- We use abstract classes and pure virtual function to achieve it
- Suppose we want to be able to provide
Printable
support for any object, we wish witout knowing its implementation at compile timestd::cout << any_object << std::endl;
any_object
must conform to thePrintable
interface
class Printable { friend ostream &operator << (ostream &, const Printable &obj); public: virtual void print(ostream &os) const = 0; virtual ~Printable() {}; // ... }; ostream &operator<<(ostream &os, const Printable &obj) { obj.print(); return os; }
To be Printable:
class AnyClass: public Printable { virtual void print(ostream &os) override { os << "Hi from AnyClass"; } };
Usage:
AnyClass *ptr = new AnyClass(); cout << *ptr << endl; void function1 (AnyClass &obj) { cout << obj << endl; } void function2 (Printable &obj) { cout << obj << endl; } function1(*ptr); // "Hi from AnyClass" function2(*ptr); // "Hi from AnyClass"
We can use capital I
plus underscore to designate an Interface:
class I_Shape { // ... };
Smart Pointers
Issues with raw pointers:
- C++ provides absolute flexibility with memory management
- Allocation
- Deallocation
- Lifetime management
- Some potentially serious problems
- Uninitialized (wild) pointers
- Memory leaks
- Dangling pointers
- Not exception safe
- Ownership:
- Who owns the pointer?
- When should a pointer be deleted?
What are smart pointers
- They are objects
- Can only point to heap-allocated memory
- Automatically call delete when no longer needed
- Adhere to RAII principles
- C++ Smart Pointers:
- Unique Pointers (
unique_ptr
) - Shared Pointers (
shared_ptr
) - Weak Pointers (
weak_ptr
) - Auto Pointers (
auto_ptr
) → deprecated
- Defined by class templates
- Wrapper around a raw pointer
- Overloaded operators
- Dereference (
*
) - Member selection (
->
) - Pointer arithmetic not supported (
++
,--
, etc)
- Can have custom deleters
Requires include:
#includes <memory>
{ std::unique_ptr<SomeClass> ptr = ...; ptr->method(); cout << (*ptr) << endl; } // Out of scope, ptr will be destroyed automatically when no longer needed
RAII - Resource Aquisition Is Initialization
- Common idiom or pattern used in software design based on container object lifetime
- RAII objects are allocated on the stack
- Resource Aquisition
- Open file
- Allocate memory
- Aquire a lock
- Is Initialization
- The resource is aquired in a constructor
- Resource relinquishing
- Happens in the destructor
- Close the file
- Deallocate the memory
- Release the lock
Unique Pointer
unique_ptr
- Simple smart pointer
unique_ptr<T>
- Points to an object of type
T
on the heap - It is unique - there can only be one
unique_ptr<T>
pointing to the object on the heap - Owns what it points to
- Cannot be assigned or copied
- Can be moved
- When the pointer is destroyed, what it points to is automatically destroyed
{ std::unique_ptr<int> p1 { new int {100} }; std::cout << *p1 << std::endl; // 100 *p1 = 200; std::cout << *p1 << std::endl; // 200 }
Methods:
ptr.get()
→ get addressptr.reset()
→ptr
is nownullptr
Using move:
std::unique_ptr<int> p1; std::unique_ptr<int> p2 = { new int {100}}; p1 = p2; // Compiler Error p1 = std::move(p2); // p2 is now ''nullptr''
make_unique
Since C++14
{ std::unique_ptr<int> p1 = make_unique<int>(100); std::unique_ptr<Account> p2 = make_unique<Account>("Joe", 500); auto p3 = make_unique<Player>("Hero", 100, 100); }
Another example:
std::vector<std::unique_ptr<Account>> accounts; accounts.push_back( make_unique<Checking_Account>("James", 1000)); accounts.push_back( make_unique<Savings_Account>("Billy", 4000, 5.2)); accounts.push_back( make_unique<Trust_Account>("Bobby", 5000, 4.5)); for (const auto &acc: accounts) { std::cout << *acc << std::endl; }
Shared Pointer
shared_ptr
- Provides shared ownership of heap objects
shared_ptr<T>
- Points to an object of type
T
on the heap - It is not unique - there can many
shared_ptr
s pointing to the same object on the heap - Establishes shared ownership relationship
- Can be assigned or copied
- Can be moved
- When the use count is zero, the managed object on the heap is destroyed
{ std::shared_ptr<int> p1 { new int {100} }; std::cout << *p1 << std::endl; // 100 *p1 = 200; std::cout << *p1 << std::endl; // 200 }
use_count
std::shared_ptr<int> p1 { new int {100} }; std::cout << "Use count: "<< p1.use_count() << std::endl; // 1 std::shared_ptr<int> p2 { p1 }; // shared ownwership std::cout << "Use count: "<< p1.use_count() << std::endl; // 2 p1.reset(); // decrement the use_count; p1 is nulled out std::cout << "Use count: "<< p1.use_count() << std::endl; // 0 std::cout << "Use count: "<< p2.use_count() << std::endl; // 1
make_shared
std::shared_ptr<int> p1 = std::make_shared<int>(100); // use_count: 1 std::shared_ptr<int> p2 { p1 }; // use_count: 2 std::shared_ptr<int> p3; p3 = p1; // use_count: 3
Weak Pointer
weak_ptr
- Provides non-owning “weak” reference
weak_ptr<T>
- Points to an object of type
T
on the heap - Does not participate in owning relationship
- Always created from a
shared_ptr
- Does not increment or decrement reference use count
- Used to prevent strong reference cycles which could prevent objects from being deleted
Problem
Circular or cyclic reference
- A refers to B
- B refers to A
- Shared strong ownership prevents heap deallocation
#include <iostream> #include <memory> using namespace std; class B; // forward declaration class A { std::shared_ptr<B> b_ptr; public: void set_B(std::shared_ptr<B> &b) { b_ptr = b; } A() { cout << "A Constructor" << endl; } ~A() { cout << "A Destructor" << endl; } }; class B { std::shared_ptr<A> a_ptr; public: void set_A(std::shared_ptr<A> &a) { a_ptr = a; } B() { cout << "B Constructor" << endl; } ~B() { cout << "B Destructor" << endl; } }; int main() { shared_ptr<A> a = make_shared<A>(); shared_ptr<B> b = make_shared<B>(); a->set_B(b); b->set_A(a); return 0; }
Output:
A Constructor B Constructor
Solution
Use one pointer as a weak pointer
#include <iostream> #include <memory> using namespace std; class B; // forward declaration class A { std::shared_ptr<B> b_ptr; public: void set_B(std::shared_ptr<B> &b) { b_ptr = b; } A() { cout << "A Constructor" << endl; } ~A() { cout << "A Destructor" << endl; } }; class B { std::weak_ptr<A> a_ptr; // make weak to break the strong circular reference public: void set_A(std::shared_ptr<A> &a) { a_ptr = a; } B() { cout << "B Constructor" << endl; } ~B() { cout << "B Destructor" << endl; } }; int main() { shared_ptr<A> a = make_shared<A>(); shared_ptr<B> b = make_shared<B>(); a->set_B(b); b->set_A(a); return 0; }
Output:
A Constructor B Constructor A Destructor B Destructor
Custom Deleters
- Sometimes when we destroy a smart pointer, we need more than just destroy the object on the heap
- These are special cases
- C++ smart pointers allow you to provide custom deleters
- Lots of ways to achieve this:
- Functions
- Lambdas
- Etc.
void my_deleter(Test *ptr) { cout << "In my custom deleter" << endl; delete ptr; } shared_ptr<Test> ptr { new Test{}, my_deleter };
With lambda expression:
shared_ptr<Test> ptr (new Test{100}, [] (Test *ptr) { cout << "In my custom deleter" << endl; delete ptr; });
Exception Handling
Basic concepts
Exception Handling:
- dealing with extraordinary situations
- indicates that an extraordinary situation has been detected or has occured
- program can deal with the extraordinary situations in a suitable manner
What causes exceptions?
- Insufficient resources
- Missing resources
- Invalid operations
- Range violations
- Underflows / overflows
- Illegal data and many others
Exception safe: when code handles exceptions
Terminology
Exception: an object or primitive type that signales that an error occured
Throwing an exception (raising an exception):
- Code detects that an error has occured or will occur
- The place where the error occured may not know how to handle error
- Code can throw an exception describing the error to another part of the program that knows how to handle the error
Catching an exception (handle exception):
- Code that handles the exception
- May or may not cause the program to terminate
C++ syntax
throw
:
- Throws an exception
- Followd by an argument
try { code that may throw an exception }
:
- We place code that may throw an exception in a
try
block - if the code throws an exception, the try block is exited
- the thrown exception is handled by a catch handler
- if no catch handler exists, the program terminates
catch(Exception ex) { code to handle exception }
:
- Code that handles exception
- can have multiple catch handlers
- may or may not cause the program to terminate
#include <stdexcept> ... throw std::runtime_error{"An error occured"};
I/O and Streams
Standard Template Library (STL)
- A library of powerful, reusable, adaptable, generic classes and functions
- Implemented using C++ templates
- Implements common data structures and algorithms
- Huge class library
- Developed by Alexander Stepanov (1994)
At it's core, it's:
- Assortment of commonly used containers
- Known time and size complexity
- Tried and tested - reusability
- Consistent, fast and type-safe
- Extensible
Elements of the STL:
- Containers
- Collection of objects or primitive types (array, vector, deque, stack, set, map, etc.)
- Algorithms
- Functions for processing sequences of elements from containers (find, max, count, accumulate, sort, etc)
- Iterators
- Generate sequeneces of elements from containers (forward, reverse, by value, by reference, constant, etc)
Simple example
#include <vector> #include <algorithm> std::vector<int> v {1, 5, 3}; // sort std::sort(v.begin(), v.end()); for (auto elem: v) { std::cout << elem << std::endl; } // reverse std::reverse(v.begin(), v.end()); for (auto elem: v) { std::cout << elem << std::endl; } // accumulate int sum {}; sum = std::accumulate(v.begin(), v.end(), 0);
Types of containers
- Seqence containers
- array, vector, list, forward_list, deque
- Associative containers
- set, multi set, map, multi map
- Container adapters
- stack, queue, priority queue
Types of iterators
- Input iterators → from the container to the program
- Output iterators → from the program to the container
- Forward iterators → navigate one item at a time in one direction
- Random access iterators - directly access a container item
Types of algorithms
- About 60 algorithms in the STL
- Non-modifying
- Modifying
Generic Programming with macros
- Generic programming
- “Writing code that works with a variety of types as arguments, as long as those arguments types meet specific syntactic and semantic requirements” - Bjarne Stoustrup
- Macros
- Function templates
- Class templates
Macros (#define
)
- C++ preprocessor directive
- No type information
- Simple substitution
Note: using macros is not necessary good practice.
#define MAX_SIZE 100 #define PI 3.14159 if (num > MAX_SIZE) { // handle } double area = PI * r * r;
Macro with arguments
#define MAX(a, b) ((a > b) ? a : b) std::cout << MAX(10, 20) << std::endl; // 20
Generic Programming with Function Templates
What is a C++ template?
- Blueprint
- Function and class templates
- Allow plugging-in any data type
- Compiler generates the appropriaate function/class from the blueprint
- Generic programming / meta-programming
max
function as a template function
- We need to tell the compiler this is a template function
- We also need to tell that
T
is the template parametertemplate <typename T> T max(T a, T b) { return (a > b) ? a : b; }
- We may also use
class
instead oftypename
template <class T> T max(T a, T b) { return (a > b) ? a : b; }
- Now the compiler can generate the appropriate function from the template
- This happens at compile-time
int a {10}; int b {20}; std::cout << max<int>(a, b);
- Many times, the compiler can deduce the type and the template parameter is not needed
std::cout << max(a, b);
- We can use almost any type we need
- In the example of
max
function, using greater-than operator, the operator must be overloaded for the type. ExamplePlayer
class withscore
, could be overloaded with this in mind
template <typename T1, typename T2> void func(T1 a, T2 b) { // implementation }
Example with struct:
template <typename T> T min(T a, T b) { return (a < b) ? a : b; } template <typename T1, typename T2> void func(T1 a, T2 b) { std::cout << a << " " << b << std::endl; } struct Person { std::string name; int age; bool operator<(const Person &rhs) const { return this->age < rhs.age; } } std::ostream &operator<<(std::ostream &os, const Person &p) { os << p.name; return os; } Person p1 {"Curly", 31}; Person p2 {"Moe", 39}; Person p3 = min(p1, p2); std::cout << p3.name << " is younger" << std::endl; // Curly is younger func(p1, p2); // Curly Moe
template <typename T> void my_swap(T &a, T &b) { T temp = a; a = b; b = temp; }
Generic Programming with Class Templates
Template classes are typically completely contained in header files. So, we would have the template class in Item.h
and no Item.cpp
file would be used.
template <typename T> class Item { private: std::string name; T value; public: Item(std::string name, T value) : name{name}, value{value} {} std::string get_name() const {return name; } T get_value() const { return value; } }; Item<int> item1 {"Joe", 100};
Creating a Generic Array Template Class
Next code is for demonstration, most of the time use std::array
instead.
template <typename T, int N> class Array { int size {N}; // how do we get the N??? T values[N]; // the N needs to ne known at compile-time! friend std::ostream &operator<<(std::ostream &os, const Array<T, N> &arr) { os << "[ "; for (const auto &val: arr.values) os << val << " "; os << "]" << std::endl; return os; } public: Array() = default; Array(T init_val) { for (auto &item: values) item = init_val; } void fill(T val) { for (auto &item: values ) item = val; } int get_size() const { return size; } // overloaded subscript operator for easy use T &operator[](int index) { return values[index]; } };
STL Containers
Containers:
- Data structures that can store object of almost any type
- Template-based classes
- Each container has member functions
- Some are specific to the container
- Others are available to all containers
- Each container has an associated header file
#include <container_type>
Containers - Common
Function | Description |
---|---|
Default constructor | Initializes an empty container |
Overloaded constructor | Initializes containers with many options |
Copy constructor | Initializes a container as a copy of another container |
Move constructor | Moves existing container to new container |
Destructor | Destroys a container |
Copy assignment (operator= ) | Copy one container to another |
Move assignment (operator= ) | Move one container to another |
size | Returns the number of elements in a container |
empty | Returns boolean of if it's empty or not |
insert | Insert an element into the container |
Other functions: swap, erase, clear, begin, end, rbegin, rend, cbegin, cend, crbegin, crend.
Container elements
What types of elements can we store in containers?
- A copy of the elements will be stored in the container
- All primitives are OK
- Element should be
- Copyable ans assignable (copy ctor / copy assignment)
- Moveable for efficiency (move ctor / move assignment)
- Ordered associative containers must be able to compare elements
operator<
,operator==
STL Iterators
- Allows abstracting an arbitrary container as a sequence of elements
- They are objects that work like pointers by design
- Most container classes can be traversed with iterators
Declaring iterators
- Iterators must be declared base on the container type they will iterate over
- Syntax:
container_type::iterator_type iterator_name;
- Examples:
std::vector<int>::iterator it1; std::list<std::string>::iterator it2; std::map<std::string, std::string>::iterator it3; std::set<char>::iterator it4;
Iterator begin and end methods
std::vector<int> vec {1, 2, 3};
std::vector<int>::iterator it = vec.begin(); // or auto it = vec.begin();
Using iterators
std::vector<int> vec {1, 2, 3}; std::vector<int>::iterator it = vec.begin(); while (it != vec.end()) { std::cout << *it << " "; ++it; } // prints: "1 2 3" for (auto it = vec.begin(); it != vec.end(); it++) { std::cout << *it << " "; }
Set:
#include <set> std::set<char> suits {'C', 'H', 'S', 'D'}; auto it = suits.begin(); while (it != suits.end()) { std::cout << *it << " " << std::end; ++it; } // prints: "C H S D"
Reverse iterator:
std::vector<int> vec {1, 2, 3}; std::vector<int>::reverse_iterator it = vec.begin(); while (it != vec.end()) { std::cout << *it << " "; ++it; } // prints: "3 2 1"
Other iterators
const_iterator
(cbegin(), cend())reverse_iterator
(rbegin(), rend())- Constant reverse iterator (crbegin(), crend())
STL Algorithms
- STL Algorithms work on sequences of container elements provided to them by iterator
- SQL has many common and useful algorithms
- Too many to describe in this section
- Many algorithms require extra information in order to do their work
- Functors (function objects)
- Function pointers
- Lambda expressions (C++11)
Algorithms and iterators
#include <algorithm>
- Different containers support different types of iterators
- Determines the types of algorithms supported
- All STL algorithms expect iterators as arguments
- Determines the sequence obtained from the container
Example with find
- The
find
algorithm tries to locate the first occurrence of an element in a container - Lots of variations
- Returns an iterator pointing to the located eleemnt or
end()
std::vector<int> vec {1, 2, 3}; auto loc = std::find(vec.begin(), vec.end(), 3); if (loc != vec.end()) { std::cout << *loc << std::endl; // 3 }
Example with for_each
struct Square_Functor { void operator()(int x) { // overload () operator std::cout << x * x << " "; } }; Square_Functor square; // Function object std::vector<int> vec {1, 2, 3}; std::for_each(vec.begin(), vec.end(), square);
Using a function pointer
void square(int x) { std::cout << x * x << " "; } std::vector<int> vec {1, 2, 3}; std::for_each(vec.begin(), vec.end(), square);
Using a lambda expression
std::vector<int> vec {1, 2, 3}; std::for_each(vec.begin(), vec.end(), [](int x) { std::cout << x * x << " "; } // lamda );
Other algorithms
Some other, not all:
std::count()
std::count_if()
std::replace()
std::all_of()
std::transform()
, ex:std::transform(str1.begin(), str1.end(), ::toupper)
Sequence Containers
- Array → See CPP Reference on std::array
- Vector → See CPP Reference on std::vector
- Deque (Double-ended Queue) → See CPP Reference on std::deque
Associative Containers
- Sets
- Maps
Container Adaptors
- Stack
- Queue
- Priority Queue
Associative Containers
- Associative containers
- Collection of stored objects that allow fast retrieval using a key
- STL provides Sets and Maps
- Usually implemented as a balanced binary tree or hashsets
- Most operations are very efficient
- Sets
std::set
std::unordered_set
std::multiset
std::unordored_multiset
Set
Set → See CPP Reference on std::set
- Similar to a mathematical set
- Ordered by key
- No duplicate elements
- All iterators available and invalidate when correspoinding element is deleted
#include <set> std::set<int> s {1, 2, 3, 4, 5}; std::set<std::string> stooges { std::string {"Larry"}, "Moe", std::string {"Curly"} }; s = {4, 6, 8, 8, 10}; // second '8' is ignored s.insert(7); // returns std::pair, ex: [7, true] (true/false = successfully inserted) s.insert(8); // will ignore if already exists in set
Autres méthodes:
s.size()
s.max_size
- No concept of
front
orback
s.find()
s.count(1)
s.clear()
s.empty()
Map
Map → See CPP Reference on std::map
- Similar to dictionary
- Elements are stored as Key-Value pairs (
std::pair
) - Ordered by key
- No duplicate elements (keys are unique)
- All iterators available and invalidate when correspoinding element is deleted
#include <map> std::map<int> s {1, 2, 3, 4, 5}; std::map<std::string, int> m1 { {"Larry", 18}, {"Moe", 25} }; std::map<std::string, std::string> m2 { {"Larry", "King"}, {"Moe", "Bar"} }; std::pair<std::string, std::string> p1 = {"James", "Mechanic"}; m2.insert(p1); m2.insert(std::make_pair("Roger", "Ranger")); m2["Roger"] = "Cook"; // update m2["Frank"] = "Singer"; // insert, key doesn't exists
Autres méthodes:
m.erase(“Frank”)
m.clear()
m.empty()
Autres containers
Container Adpators
- std::stack → stack
- std::queue → queue
- std::priority_queue → priority_queue
Lambda Expressions
Since C++11
Motivation
Prior to C++11:
- Function objects and function pointers
- We often write many short function that control algorithms
- The short functions would be encapsulated in small classes to produce function objects
- Many times, the classes and functions are far removed from where they are used, leading to modification, maintenance and testing issues
- Compiler cannot effectively inline these functions for efficiency
Structure of a Lamda Expression
Syntax:
[] () { std::cout << "Hi"; }(); // immediately executed auto l = [] (int x) { std::cout << x; }; l(10);
Using pointers
Capture List
An empty capture list would result in a stateless lambda expression.
Examples
Using lambda expression as function parameters
#include <functional> // C++14 -> void below is return type, int is parameter type void foo(std::function<void(int)> l) { l(10); } // Also C++14 void foo(void (*l)(int)> l) { l(10); } // C++20 void foo(auto l) { l(10); }
Returning lambda expression from functions
#include <functional> std::function<void(int)> foo() { return [] (int x) { std::cout << x; }; } // or void (*foo())(int) { return [] (int x) { std::cout << x; }; } // or auto foo() { return [] (int x) { std::cout << x; }; }
With predicate
void print_if(std::vector<int> nums, bool (*predicate)(int)) { for (int i: nums) { if (predicate(i)) { std::cout << i; } } } int main() { std::vector<int> nums {1, 2, 3}; print_if(nums, [] (auto x) { return x % 2 == 0; }); print_if(nums, [] (auto x) { return x % 2 != 0; }); return 0; }
Stateful Lamba Expressions
Using non-empty capture list.
Transformation of lambda expression by the compiler
Lamda definition
int y {10}; auto l = [y] (int x) { std::cout << x + y; };
Compiler-generated closure
class CompilerGeneratedName { private: int y; public: CompilerGeneratedName(int y): y{y} {}; void operator() (int x) const { std::cout << x + y; } }
const
in generated class avoid modifying member values.
For a large number of capture variables:
[=]
→ Default capture by value[&]
→ Default capture by reference[this]
→ Default capturethis
object by reference[=, &x]
→ Default capture by value, but capturex
by reference- Other variations exists
Lambdas and the STL
Some code samples to show lambda uses with STL.
for_each
Non-modifying sequence operation, displays each element of nums.
std::vector<int> nums {10, 20, 30, 40, 50}; std::for_each(nums.begin(), nums.end(), [] (int num) { std::cout << num << " "; });
transform
std::vector<int> test_scores {93, 88, 75, 68, 65}; int bonus_points {5}; std::transform(test_scores.begin(), test_scores.end(), test_scores.begin(), [bonus_points] (int score) { return score += bonus_points; }); // Display updated test_scores for (int score : test_scores) { std::cout << score << " "; }
Enumerations
What is an enumeration?
- A user-defined type that models a set of contant integral values
- Days of the week
- Ths suits in a deck of cards (Clubs, Hearts, Spades, Diamonds)
- State of a system
- Etc
Structure
enum-key enum-name : enumerator-type { };
Simplest enumerator:
enum {Red, Green, Blue}; // Implicit initialization: // Red = 0, Green = 1 and Blue = 2
enum {Red = 1, Green = 2, Blue = 3}; // Explicit initialization enum {Red = 1, Green, Blue}; // Explicit/Initialisation initialization // Green = 2 and Blue = 3
Enumetator type is deduced with width of bits required.
Anonymus enum provides no type safety:
enum {Ref, Green, Blue}; int my_color; my_color = Green; // valid my_color = 4; // valid
Named:
enum Color {Ref, Green, Blue}; Color my_color; my_color = Green; // valid my_color = 4; // not valid
Unscoped Enumerations
enum State { Engine_Failure, Inclement_Weather, Nominal }; std::underlying_type_t<State> user_input; std::cin >> user_input; switch (user_input) { case Engine_Failure: state = State(user_input); break; case Inclement_Weather: state = State(user_input); break; case Nominal: state = State(user_input); break; default: std::cout << "User input is not a valid state."; }
With operator overload:
enum State { Engine_Failure, Inclement_Weather, Nominal }; std::istream& operator>>(std::istream& is, State& state) { std::underlying_type_t<State> user_input; is >> user_input; switch (user_input) { case Engine_Failure: state = State(user_input); break; case Inclement_Weather: state = State(user_input); break; case Nominal: state = State(user_input); break; default: std::cout << "User input is not a valid state."; } return is; } // ... State state; std::cin >> state;
Scoped Enumerations
Enumerator is qualified.
enum class enum-name : enumerator-type {};
Problem:
enum Whale { Blue, Beluga, Gray }; enum Shark { Greatwhite, Hammerhead, Bull }; if (Beluga == Hammerhead) { std::cout << "A beluga whale is equivalent to a hammerhead shark."; }
Name clashes, here with Blue
:
enum Whale { Blue, Beluga, Gray }; enum Shark { Greatwhite, Hammerhead, Bull, Blue }; // Error: Blue already defined
Solution:
enum class Whale { Blue, Beluga, Gray }; Whale whale = Whale::Beluga; switch (whale) { case Whale::Blue: std::cout << "Blue whale"; break; case Whale::Beluga: std::cout << "Beluga whale"; break; case Whale::Gray: std::cout << "Gray whale"; break; default: std::cout << "Unknown whale"; }
Using scoped enumerator values
enum class Item { Milk = 350, Bread = 250, Apple = 132 }; int milk_code = int(Item::Milk); // milk_code = 350 // or int milk_code = static_cast<int>(Item::Milk); int total = int(Item::Milk) + int(Item::Bread); // total = 600 std::cout << underlying_type_t<Item>(Item::Milk); // 350
Assert
#include <cassert> ... assert(!Empty());