Le C++ moderne existe depuis C++11, c'est-à-dire depuis 2011.
Certains IDE d'intérêt pour C++:
sudo apt install build-essential
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#include <iostream> #include "myfile.h" #if #elif #else #endif #ifdef #ifndef #define #line #error #pragma
// Single line comment /* Multi-line comment */
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.
std
est le nom pour namespace standard
::
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";
VariableType variableName;
Règles:
int age; // uninitialized int age = 21; // C-like initialization int age = (21); // Constructor initialization int age {21}; // C++11 list initialization syntax
#include <iostream> int age {18}; // Global int main() { int age {16}; // local, shadowing global one }
Liste non-exhautive (voir Introduction to fundamental data types) :
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(int); int age {31}; sizeof(age); sizeof(long long);
Constantes de minimums et maximums:
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
Multi-dimentional arrays
int movie_rating [3][4] { {0,3,4,5}, {0,2,5,5}, {0,3,1,5} };
Points importants:
<type>[size]
, e.g. int arr[]
est int[5]
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
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);
#include <vector> vector <char> vowels; vector <int> test_scores;
C-Style Strings
String Literals
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
#include <cstdlib>
Inclus des fonctions pour convertir des chaines C-Style à integer, float, long, etc.
std::string
is a class in the STL
#include <string>
#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
Définir un regex:
std::regex regex(R"(\d{2}));
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; }
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.
Define functions before calling them:
Use function prototypes:
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; }
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:
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
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; }
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 }
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; }
Local or Block scope:
Static local variables:
static int value {10};
Global scope:
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 int add_numbers(int a, int b) { // definition return a + b; } int main() { int result {0}; result = add_numbers(100, 200); // call return 0; }
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; }
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; }
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) { ... }
Qu'est-ce qu'un pointeur:
Qu'est-ce qui peut être à cette adresse ?
Pour utiliser la valeur dont le pointeur pointe, on doit connaitre son type.
Pourquoi utiliser 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.
nullptr
(C++11) représente l'adresse zero.int *int_ptr {}; int *int_ptr {nullptr};
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
.
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
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
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 s1 {"Some string"}; string *p1 {&s1};
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
Allocating storage from heap at runtime.
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;
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
++
: 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; }
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
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"; }
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; }
Qu'est-ce qu'une référence:
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-value:
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
const
nullptr
valueconst
pointer to const
nullptr
valuenullptr
const
referencenullptr
class Account { std::string name; double balance; bool withdraw(double amount); bool deposit(double amount); };
Account account1; Account account2; Account accounts[] { account1, account2 }; std::vector<Account> accounts { account1 }; accounts.push_back(account2);
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)
Account *account1 = new Account(); (*account1)->balance; (*account1)->deposit(100.0);
Account *account1 = new Account(); account1->balance; account1->deposit(100.0);
class Player { private: std::string name; int health; int xp; public: void talk(std::string text_to_say); bool is_dead(); };
Class_name::method_name
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; }
#ifndef _ACCOUNT_H_ #define _ACCOUNT_H_ // Account class declaration #endif
Ou
#pragma once
Constructors:
Destructors:
~
)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; } };
{ 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.
Player frank; Player *enemy = new Player;
// 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; }
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; }
#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; }
#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; }
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);
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();
Player hero {"Hero", 100, 100}; Player another_hero {hero}; // A copy of 'hero' is made
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} { }
Default Copy Constructor
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; }
int total {0}; total = 100 + 200;
100 + 200
is evaluated and 300
stored in an unnamed temp value300
is then stored in the variable total
When is it useful?
R-Values:
&&
#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; }
this
is a reserved keywordthis
pointer*this
) to yield the current objectconst 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; // ... };
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; }
class
we can declare a struct
struct
comes from the C programming languageclass
expectpublic
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};
Considerations:
friend
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; };
What is Operator Overloading?
See Introduction to operator overloading for more details.
Mystring s1 {"Joe"}; Mystring s2 = s1; // NOT assignment, same as Mystring s2{s1}; s2 = s1; // assignment
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; }
Mystring s1; s1 = Mystring {"Joe"}; // Move assignment
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; }
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;
What is it and why is it used?
class Account { // balance, deposit, withdraw }; class Savings : public Account { // interest rate, specialized withdraw };
class Base { // Base class members }; class Derived: access-specifier Base { // Derived class members };
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; }
other
will be sliced.Derived(const Derived &other) : Base(other), doubled_value {other.doubled_value} { cout << "Derived copy constructor" << endl; }
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 } };
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
class Department_Chair : public Faculty, public Administrator { // code };
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()
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.
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; }
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; }
Declaring virtual functions:
class Account { public: virtual void withdraw(double amount); // ... };
Declaring virtual functions (derived classes):
class Checking: public Account { public: virtual void withdraw(double amount); // ... };
Solution / Rule:
class Account { public: virtual void withdraw(double amount); virtual ~Account(); // ... }; class Savings: public Account { public: virtual void withdraw(double amount); virtual ~Savings() { } };
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 differentsay_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() {} };
C++11 provides the final specifier
Syntax:
class MyClass final { // ... };
Syntax on derived class:
class Derived final: public Base { // .. }
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 };
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
Abstract class:
Concrete class:
Abstract Base Class:
Pure virtual function:
=0
in its declaration virtual void function() = 0;
Shape
: virtual void draw() = 0;
Printable example:
Printable
support for any object, we wish witout knowing its implementation at compile time std::cout << any_object << std::endl;
any_object
must conform to the Printable
interfaceclass 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 { // ... };
Issues with raw pointers:
unique_ptr
)shared_ptr
)weak_ptr
)auto_ptr
) → deprecated*
)->
)++
, --
, etc)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
unique_ptr
unique_ptr<T>
T
on the heapunique_ptr<T>
pointing to the object on the heap{ 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 now nullptr
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''
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_ptr
shared_ptr<T>
T
on the heapshared_ptr
s pointing to the same object on the heap{ std::shared_ptr<int> p1 { new int {100} }; std::cout << *p1 << std::endl; // 100 *p1 = 200; std::cout << *p1 << std::endl; // 200 }
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
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_ptr
weak_ptr<T>
T
on the heapshared_ptr
Circular or cyclic reference
#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
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
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:
What causes exceptions?
Exception safe: when code handles exceptions
Exception: an object or primitive type that signales that an error occured
Throwing an exception (raising an exception):
Catching an exception (handle exception):
throw
:
try { code that may throw an exception }
:
try
block
catch(Exception ex) { code to handle exception }
:
#include <stdexcept> ... throw std::runtime_error{"An error occured"};
At it's core, it's:
Elements of the STL:
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);
Macros (#define
)
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;
#define MAX(a, b) ((a > b) ? a : b) std::cout << MAX(10, 20) << std::endl; // 20
What is a C++ template?
max
function as a template function
T
is the template parameter template <typename T> T max(T a, T b) { return (a > b) ? a : b; }
class
instead of typename
template <class T> T max(T a, T b) { return (a > b) ? a : b; }
int a {10}; int b {20}; std::cout << max<int>(a, b);
std::cout << max(a, b);
max
function, using greater-than operator, the operator must be overloaded for the type. Example Player
class with score
, could be overloaded with this in mindtemplate <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; }
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};
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]; } };
Containers:
#include <container_type>
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.
What types of elements can we store in containers?
operator<
, operator==
container_type::iterator_type iterator_name;
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();
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"
const_iterator
(cbegin(), cend())reverse_iterator
(rbegin(), rend())Algorithms and iterators
#include <algorithm>
find
algorithm tries to locate the first occurrence of an element in a containerend()
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 }
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 );
Some other, not all:
std::count()
std::count_if()
std::replace()
std::all_of()
std::transform()
, ex: std::transform(str1.begin(), str1.end(), ::toupper)
std::set
std::unordered_set
std::multiset
std::unordored_multiset
Set → See CPP Reference on std::set
#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
front
or back
s.find()
s.count(1)
s.clear()
s.empty()
Map → See CPP Reference on std::map
std::pair
)#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()
Since C++11
Prior to C++11:
Syntax:
[] () { std::cout << "Hi"; }(); // immediately executed auto l = [] (int x) { std::cout << x; }; l(10);
An empty capture list would result in a stateless lambda expression.
#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); }
#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; }; }
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; }
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 capture this
object by reference[=, &x]
→ Default capture by value, but capture x
by referenceSome code samples to show lambda uses with STL.
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 << " "; });
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 << " "; }
What is an enumeration?
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
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;
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
#include <cassert> ... assert(!Empty());