“Appunti di programmazione ad oggetti” Francesco Ranzato
Un programma è costituito da
Focus su algoritmi (programmazione procedurale) Focus su dati (programmazione ad oggetti)
Qualità del software
Un linguaggio si dice oggetti quando ha le 3 seguenti caratteristiche INCAPSULAMENTO, un programma può essere usato dentro un altro EREDITARIETÀ, un programma può ereditare le funzioni di un altro POLIMORFISMO, la chiamata di funzione si basa a runtime
Template per programmazione generica, ovvero indipendente del tipo delle variabili. Si gestiscono le eccezioni.
C++
Java
#include <iostream.h>
using namespace std;
int main() {
cout << "Hello world!" << endl;
}
#
direttiva del pre-processore
main
alla base del code stack
<<
operatore di output
#include <iostream.h>
int main() {
std::cout << "Hello world!" << std::endl;
}// preferibile
Namespace Quando due variabili hanno lo stesso nome, li possiamo distinguere dal loro scope.
namespace ns_name{
struct X;
}
Alias di namespace
namespace alias = ns_name;
Direttiva di uso
using namespace ns_name;
Dichiarazione di uso
using ns_name::X;
Invocazione compilatore > gcc -xc++
Array di caratteri
char *str = "ecco una stringa c"; // automaticamente aggiunge /0
const char *st_c = "hello"; // a solo lettura
for(int i=0; *(str+i); i++)
Classe stringa (istantazione di templare di classe typedef basic_string<char> string
)
#include <string>
int main() {
string str = "ecco una stringa c++";
int length = str.size();
}
Un tipo di dato viene detto astratto (ADT), quando la sua semantica è separata dalla sua implementazione. La sua rappresentazione interna è inaccessibile. ADT = Valori + Operazioni (metodi pubblici)
Struct non è un ADT, tutto è accessibile.
Principio dell’Information Hiding Metodo progettuale di segregazione delle informazioni che hanno più possibilità di cambiare in modo da proteggere le altre parti del programma da modifiche estensive quando viene cambiato il design
Classe implementa gli ADT. La dichiarazione di classe consiste da
class orario {
private: // specificatore di accesso
short int seconds;
public:
orario();
orario(short int o, short int m=0, short int s=0);
short int Ore();
short int Minuti();
short int Secondi();
// hanno un argomento implicito oggetto di invocazione del metodo
orario::orario() {
sec = 0;
}
orario::orario(short int o, short int m, short int ) {
if(o<24 && m<60 && s<60) {
sec = s + m*60 + o*3600;
}
}
short int orario::Ore() {
return sec / 3600;
}// preferibile scritta separatamente dalla dichiarazione perché sarebbe inline e potrebbe essere interpretata come macro
short int orario::Minuti() {
return (sec / 60) % 60;
}
short int orario::Secondi() {
return sec % 60;
}
int main() {
orario o1;
orario o2(15, 30, 15);
orario o3 = orario(14, 20, 05); // costruttore di copia
orario *ptr = new orario; // new è la malloc di C++
}
};
Le diverse istanze di oggetti sono salvati individualmente mentre i metodi sono salvati una sola volta.
Oggetto di invocazione e metodo invocato sono legati dal parametro implicito *C this
.
Costruttore di Copia C(const T&)
orario o = orario(14, 20, 5);
crea una istanza di oggetto anonimo e temporaneo (senza l-valore) che viene poi assegnato a un oggetto.
N.B. Diverso da o = orario(14, 20, 5);
che è solo una assegnazione
orario copia1 = o;
orario copia2(o); // preferibile
Costruttore Standard
quando non definiamo un costruttore, è già presente uno standard di default. Se definiamo un costruttore con argomenti, orario o;
non è più un codice valido.
COMPCERT Xavier Lerox (compilatore ottimizzato che ritorna prova matematica)
new e delete in C++ malloc e free in C
Argomenti di default
funzione func(int a, int b = 0, int c = 0);
Tutti gli argomenti a destra di quello con default devono avere default.
Conversione Implicita
orario s,t;
s = 8; // equivale a s = orario(8);
t = 8+12; // equivale a t = orario(8+12);
Quando una variabile non è dello stesso tipo dell’oggetto a cui è assegnato, avviene una conversione implicita dove viene convertito in un oggetto ammissibile.
Operatori Espliciti di Conversione
class orario {
public:
operator int() {return sec;}
. . .
}
orario o(12, 25);
int s = o; // richiama implicitamente int() su o
explicit
blocca le conversioni implicite, utile quando non hanno senso.
Metodi e oggetti di invocazione costanti
class orario {
public:
. . .
orario UnOraPiuTardi(); // sola lettura
void AvanzaUnOra(); // lettura-scrittura
orario orario::UnOraPiuTardi() const{ // non causa side-effects
orario tmp;
tmp.sec = (sec/3600) % 86400;
return tmp;
}
void orario::AvanzaUnOra() {
sec = (sec/3600) % 86400;
}
}
const orario LE_TRE(15);
orario t;
t = LE_TRE.UnOraPiuTard();
Nei metodi costanti this
è di tipo const *C
.
Ai oggetti costanti e nei metodi costanti si possono invocare solo metodi anche essi costanti.
L’eccezione sono i costruttori.
\(\boxed{\text{Make everything const, as much as possible.}}\)
Riferimento
int x = 2;
int& a = x; // tipo rifermento, ALIAS
int& q = 4; // ILLEGALE
int y = 3;
a = y; // x=3
const int& r=x; // sola lettura
const int& rq=4; // LEGALE
Puntatori
int *p1 = &x;
p1 = &y; // LEGALE
int *const p2 = &x; // costante l-valore
p2 = &y; // ILLEGALE
const int *p3 = &x; // protegge r-valore
p3 = &y; // LEGALE
*p3 = 5; // ILLEGALE
const int *const p = &x; // sola lettura
Differenza con i puntatori
NULL
Passaggio dei parametri come riferimento (invece dei puntatori) modifica e legge le variabili passate.
// PASSAGGIO PER RIFERIMENTO TIPO COSTANTE
void fun1(const Big& r);
void fun2(Big v); // per valore, da evitare
Big b(. . .);
fun1(b); // copia di riferimento a Big
fun2(b); // costruttore di copia di Big
Ritornare un riferimento
int v[] = {3, 2, 6, 0, 5}; // variabile globale
int& setValue(int i) {
return v[i];
}
int main() {
setValue(2) = 5; // modifica v[2]
}
Non puoi ritornare un riferimento di una variabile locale.
Ritorno di riferimento costante
const int& f() {return 4;}
int main() { f(); }
// warning: ritorna riferimento a un oggetto temporaneo
const int& f(int* p) {return *p;}
int main() { f(new int(4)); } // LEGALE
Esercizio
class C {
private:
int x;
public:
C(int n=0);
C F(C obj);
C G(C obj) const;
C H(C& obj);
C I(const C& obj);
C J(const C& obj) const;
C::C(int n=0) { x=n; }
C C::F(C obj) { C r; r.x = obj.x + x; return r; }
C C::H(C& obj) { obj.x += x; return obj }
// r è temp, C è senza l-valore
C C::G(C obj) const { C r; r.x = obj.x + x; return r; }
C C::I(const C& obj) { C r; r.x = obj.x + x; return r; }
C C::J(const C& obj) const { C r; r.x = obj.x + x; return r; } // ha l-valore
};
int main() {
C x, y(1), z(2), const C v(2);
z=x.F(y);
// v.F(v) non funziona, const può usare solo metodi const
v.G(y);
(v.G(y)).F(x); // G() ritorna C, quindi F() funziona
(v.G(y)).G(x); // conversione C a const C
// x.H(v) non funziona, cerca funzione c::H(const C&)
// x.H(z.G(y)) non funziona, riferimento vuole l-valore
x.I(z.G(y)); // riferimento const non ha bisogno di l-valore
x.J(z.G(y));
v.J(z.G(y));
}
Metodo che non fa uso di this
e dato di una classe utilizzabile ovunque
class orario {
public:
static orario OraDiPranzo();
static int Sec_di_una_ora; // invece delle variabili globali
static int Sec_di_un_giorno;
orario orario::OraDiPranzo() {
return orario(13,15);
}
};
// da inizializzare fuori
int orario::Sec_di_una_ora = 3600;
int orario::Sec_di_un_giorno = 86400;
Nel caso vuoi rendere i dati statici costanti, si possono inizializzare inline dentro la classe.
class orario {
public:
. . .
static const int Sec_di_una_ora = 3600;
. . .
};
I metodi di una classe hanno accesso della parte privata anche delle istanze della classe passati per parametro.
Funzionano come i metodi usati finora.
class orario {
public:
...
orario operator+ (orario) const;
orario orario::operator+(orario o) const {
orario aux;
aux.sec = (sec + o.sec) % 86400;
return aux;
}
};
int main {
...
orario ora(22,45);
orario DUE_ORE_E_UN_QUARTO(2,15);
ora = ora + DUE_ORE_E_UN_QUARTO;
}
Regole di overloading
Se è un metodo allora l’oggetto d’invocazione è il primo argomento.
[OPERATORS]
+ - * / % == != < <= > => ++ --
<< >> = -> [] () & new delete
Gli operatori “=“, “[]” e “->”
devono avere parametro this
, quindi il loro overload è metodo interno
++b
incrementa e ritorna b
b++
ritorna b e incrementa
xcasa
-, ==, > e <
per la classe orario+, -, ==, > e <
per la classe complessoOperatore di assegnazione =
C& operator =(const C&)
L’operatore di assegnazione per una classe, fa assegnamento membro per membro e i suoi sotto-oggetti. Ogni sotto-oggetto è assegnato in un modo appropriato per tipo.
int x=1, y=4, z=7;
x = y = z;
cout << x << " " << y << " " << z; // ritorna 7 7 7
Il costruttore di copia viene invocato
int main() { // con g++ ottimizzato quante invocazioni di c. di copia? C c; fun(c); // 2 invocazioni
C y = fun(c); // 2 invocazioni, non 3
C z; z = fun(c); // 2 invocazioni
fun(fun(c)); // 3 invocazioni, non 4
C x;
C y = f(f(x)); // 7 invocazioni } ``` `> g++ -fno-elide-constructors` al compilatore rimuove questa ottimizzazione
Funzionalità di stampa
Viene definito facendo overloading dell’operatore di output <<
.
Non funziona come metodo di una classe perché sarebbe obbligato ad avere il this
come primo parametro. È funzione esterna.
// deve essere invocato a cascata, quindi ritorna lo stream
ostream& operator<<(ostream& os, const orario& o) {
return os << o.Ore() << ":" << o.Minuti() << ":"
<< o.Secondi();
}
Possiamo raggrupparlo insieme alla classe con un namespace.
(Martedì iniziamo alle 14:00)
Operatore di somma + Come metodo interno, NON È COMMUTATIVA
class orario {
public:
orario orario::operator+(const orario& o) const;
}
int main() {
orario t(12,20), s;
s = t+4; // FUNZIONA
s = 4+t; // l’ordine è importante: 4 è un temp anonimo, NON FUNZIONA
}
Come funzione esterna
orario operator+(const orario& t, const orario& s);
int main() {
orario t(12,20), s;
s = 4+t;
s = 4+5;
}
MA! L’operatore non ha più accesso ai parametri interni di orario.
// File Point.h
class Point{
private:
double _x, _y, _z;
public:
Point(double x, double y, double z);
Point();
double getX() const;
double getY() const;
double getZ() const;
void negate();
double norm() const;
};
// File Point.opp
#include "Point.h"
// conversione double => Point
Point::Point(double x=0.0, double y=0.0, double z=0.0) {
_x=x; _y=y; _z=z;
}
double Point::getX() const { return _x; }
double Point::getY() const { return _y; }
double Point::getZ() const { return _z; }
void Point::negate() {
_x=-_x; _y=-_y; _z=-_z;
}
double Point::norm() const {
return sqrt(_x^2+_y^2+z^2);
}
Preprocessore, sostituzione sintattica
#include
: direttive di inclusione#define
: direttive di macro-> Inclusione multipla di file header
#ifdef ORARIO_H
#define ORARIO_H
class orario {
...
};
#endif
> g++ -c orario.cpp
Compila e non linka, produce il file .o
Source —-> Object file
Source <-/- Object file
Impossibile ottenere source esatto, ma esistono decompiled per reverse engineering
Code obfuscation, codice inutile usato per offuscare il source code dal reverse engineering.
Qt Creator Valgrind, Valkyria
Relazione has-a Un oggetto è campo dato di un altro oggetto.
#ifndef TELEFONATA_H
#define TELEFONATA_H
#include <iostream>
#include "orario.h"
class Telefonata {
private:
orario inizio, fine;
const int numero; // non possono essere modificati quindi deve essere assegnato in inizializzazione
public:
telefonata(orario,orario,int);
telefonata();
orario Inizio() const;
orario Fine() const;
bool operator==(const telefonata&) const;
};
ostream& operator<<(ostream&, const telefonata&);
#endif
La posizione in memoria dipende dall’ordine di dichiarazione. [obj orario | int] Parametri oggetto sono sempre costruiti di default.
#include “telefonata.h”
// lista di inizializzazione, costruttore di copia esplicita
telefonata::telefonata(const orario& i, const orario& f, const int& n) : inizio(i), fine(f), numero(n) {}i
bool telefonata::operator==(const telefonata& t) const {
return inizio == t.inizio &&
fine == t.fine &&
numero == t.numero;
}
Contenitori sono implementati in congiunzione con gli iterattori. Da STL (Standard Template Library): list, vector, map, …
#ifndef BOLLETTA_H
#define BOLLETTA_H
#include "telefonata"
class bolletta {
public:
bolletta();
bolletta(const bolletta&);
~bolletta();
bool Vuota() const;
void Aggiungi_Telefonata(const telefonata&);
void Togli_Telefonata(const telefonata&);
telefonata Estrai_Una();
bolletta& operator=(const bolletta& y);
private:
class nodo {
public:
nodo();
nodo(const telefonata, nodo*);
telefonata info; // campi possono ricadere in public: perchè
nodo* next; // siamo in private: di bolletta
};
nodo* first; // puntatore al primo nodo della lista
static nodo* copia(nodo*);
static void distruggi(nodo*);
};
#endif
I namespace sono annidabili; per accedere a nodo usiamo bolletta::nodo.
#include "bolletta.h"
bolletta::nodo::nodo() : next(nullptr) {} // da c++ 11, prima era 0
bolletta::nodo::nodo(const telefonata& t, nodo* s) : info(t), next(s) {}
bolletta::bolletta() : first(nullptr) {}
bool bolletta::Vuota() const {
return first == nullptr;
}
void bolletta::Aggiungi_Telefonata(const telefonata& t) {
first = new nodo(t,first);
}
void bolletta::Togli_Telefonata(const telefonata & t) {
nodo* p=first, *prec=nullptr;
while(p && !(p->info == t)) {
prec = p;
p = p->next;
}
if(p) {
if(prec==nullptr)
first = p->next;
else
prec->next = p->next;
delete p;
}
}
telefonata bolletta::Estrai_Una() {
// precondizione: bolletta non vuota
nodo* p = first;
first = first->next;
telefonata aux = p->info;
delete p;
return aux;
}
Attenzione: Aggiungi_Telefonata e Togli_Telefonata possono causare side effects. Se assegno una bolletta b1 a una b2 (copia campo per campo), questi metodi applicati su una delle due, causa side effects anche l’altra. b1 e b2 hanno puntatori che puntano a uno stesso spazio di memoria (aliasing).
Dobbiamo fare DEEP COPY.
class bolletta {
public:
. . .
bolletta(const bolletta&);
bolletta& operator=(const bolletta& y);
private:
. . .
static nodo* copia(nodo*);
static void distruggi(nodo*);
};
// ASSEGNAZIONE PROFONDA
bolletta::nodo* bolletta::copia(nodo* p) {
if(p==nullptr) return nullptr;
nodo* primo = new nodo(p->info,nullptr);
nodo* q = first;
while(p->next != nullptr) {
p = p->next;
q->next = new nodo(p->info,nullptr);
q = q->next;
}
delete q;
}
bolletta::nodo* bolletta::copia(nodo* p) {
if(!p) return nullptr;
else
return new nodo(p->info,copia(p->next));
}
bolletta::nodo* bolletta::distruggi(nodo* p) {
if(p) {
distruggi(p->next);
delete p;
}
}
bolletta& bolletta::operator=(const bolletta& b) {
if(this != &b) {
distruggi(first);
first = copia(b.first);
}
return *this;
}
// COSTRUZIONE DI COPIA PROFONDA
bolletta::bolletta(const bolletta& b) : first(copia(b->first)) {}
Funzioni esterne costruita con l’interfaccia disponibile
orario Somma_Durate(bolletta b);
bolletta Chiamate_A(int num, bolletta& b);
bolletta Chiamate_A(int num, bolletta& b) {
bolletta selezionate, resto;
while (!b.Vuota()) {
telefonata t = b.Estrai_Una();
if(t.Numero == num)
selezionate.Aggiungi_Telefonata(t);
else
resto.Aggiungi_Telefonata(t);
}
b = resto;
return selezionate; // temporaneo anonimo da ritornare fa garbage
}
Al ritorno di un oggetto temporaneo anonimo si fa un costruttore di copia e il temporaneo rimane come garbage.
Lifetime di un oggetto In linguaggi con garbage collector, gli oggetti sono in uno heap e il loro lifetime è determinata Le variabili statiche si trovano in uno spazio di memoria statica che dura per tutta la durata del programma. Le variabili locali automatiche sono allocate e deallocate nel call stack. Le variabili dinamiche sono allocate dinamicamente nello heap.
| Tipo | Nascita | Morte | | ——— | —————— | ———————- | | Statiche | Avvio programma | Fine programma | | Locali | { } nel call stack | Pop { } dal call stack | | Dinamiche | Costruzione, new | Distruzione, delete |
Distruttore standard: invoca il “distruttore” per tutti i campi dati della classe, nell’ordine inverso della loro dichiarazione.
DEEP DESTRUCTION
class bolletta {
public:
. . .
~bolletta();
. . .
}
bolletta::~bolletta() {
distruggi(first); // applicata al delete di bolletta
}
// ALTRA OPZIONE - - - - - - -
bolletta::nodo::~nodo() {
if(next != nullptr)
delete next; // chiamata ricorsiva
}
bolletta::~bolletta() {
if(first) delete first;
} // ATTENZIONE: questa opzione, se si chiama remove di un singolo nodo c’è delete al nodo e nel caso sia first, tutta la lista diventa irraggiungibile
Nel g++ e clang l’ordine di distruzione nelle funzioni è variabili locali, parametri passati per valore e oggetto anonimo ritornato per valore.
Rule of three Se una classe ridefinisce una dei tre
- Distruttore
- Costruttore di copia
- Operatore di assegnazione probabilmente tutte e tre devono essere ridefinite.
int arrayStatico[5] = {3,2,-3};
int *arrayDinamico = new int[5];
arrayDinamico[0] = 3;
*(arrayDinamico+1) = 2; // è uguale a arrayDinamico[1]
delete[] arrayDinamico; // dealloca tutto l’array
I file .h e .opp sono consegnati al cliente. Nel file .h si vede la definizione delle parti private. Come nascondiamo le informazioni?
// file C_handle.h
class C_handle{
public:
// parte pubblica
private:
class C_private; // dichiarazione incompleta
C_privata* punt; // solo puntatore dell’oggetto
}
I puntatori posso essere definiti anche solo dopo una dichiarazione incompleta. La dichiarazione completa serve quando dobbiamo deferenziarli. Oggetti così definiti si trovano sullo heap e sono puntati dall’handle.
// file C_handle.cpp
class C_handle::C_privata{
// parte privata
}
Friend Function La funzione Friend viene usata per dare accesso alla parte privata della classe. Attenzione: Se è usato molto spesso, c’è probabilmente un problema di design.
// file bolletta.h
class bolletta{
. . .
friend ostream& operator<<(ostream&, const bolletta&);
. . .
}
// file .cpp
ostrema& operator<<(ostream& os, const bolletta& b)
(2003) Le classi annidate non hanno accesso alla classe contenitrice. (2011) Una classe privata è un membro della classe contenitrice e come essa ha accesso alla parte privata.
La relazione di amicizia non è simmetrica e non è transitiva.
class contenitore {
private:
class nodo{
. . .
};
nodo* first;
public:
class iterator {
friend class contenitore; // NECESSARIA
private:
contenitore::nodo* punt;
public:
bool operator==(const iterator& i) const;
bool operator!=(const iterator& i) const;
iterator& operator++();
// nessun costruttore, si usa quello standard
};
iterator begin() const;
iterator end() const; // ritorna past-end
int& operator[](const iteratore&) const;
}
L’iteratore è annidato nella parte pubblica della classe che itera. La classe deve essere definita con friend nell’iteratore.
bool contenitore::iterator::operator==(const contenitore::iterator& i) const {
return punt == i.punt;
}
bool contenitore::iterator::operator!=(const contenitore::iterator& i) const {
return punt != i.punt;
}
contenitore::iterator& contenitore::iterator::operator++() {
if (punt) punt=punt->next; return *this;
}
contenitore::iterator contenitore::begin() const {
iteratore aux;
aux.punt = first; // per amicizia
return aux;
}
contenitore::iterator contenitore::end() const {
iteratore aux;
aux.punt = 0; // per amicizia
return aux;
}
int& contenitore::iterator::operator[](const contenitore::iterator& it) const {
return it.punt->info; // per amicizia, nessun controllo su it.punt
}
Gli iteratori sono spesso usati per parse il contenitore (ad esempio nei cicli for).
int main() {
contenitore c;
for(auto it=c.begin(); it!=c.end(); ++it)
// it è puntatore di un oggetto in c
}
Overloading dell’operatore di -> Operatore di selezione solitamente ritorna un puntatore a classe.
class bolletta{
public:
class iteratore{
friend class bolletta;
private:
bolletta::nodo* punt;
public:
telefonata* operator->() const { return &(punt->info); }
telefonata& operator*() const { return punt->info; }
// solo il puntatore è costante, con questi metodi possiamo modificare la telefonata a cui punta
}
};
Tipi di variabile
Standard: sizeof(char)<=sizeof(short)<=sizeof(int)<=sizeof(long)<=sizeof(long long)
Conversione di tipo Ogni linguaggio di programmazione ha le proprie regole sulla conversione di tipo. Caratteristica conversioni
Conversione implicita
Un’espressione e è convertibile implicitamente a un tipo T quando T può essere costruito da copia da e. T t=e
// T& => T
// T[] => T*
int[2] a={3,1}; int* p=a;
// T* => void*
// puntatore generico, senza tipo
int* p=&x; void* q=p;
// T => const T
int x=5; const int y=x;
// const NPR => NPR
// NPR = tipo non puntatore o riferimento
const int x=5; int y=x;
int* const p=&x; int* q=p;
// T* => const T*
int* p=&x; const int* q=p;
// T => const T&
const int& x=5;
// e quelli tra TIPI PRIMITIVI con WIDE CONVERSION
Attenzione: se convertiamo i tipi primitivi dobbiamo tenere conto sul cambio di rappresentazione quindi, anche se funziona implicita, è meglio segnarla con uno static_cast.
char c = 's';
int x = static_cast<int>(c);
Conversione esplicita
static_cast
const_cast
, per rimuovere l’attributo const di un oggettoreinterpret_cast
, converte il puntatore di un tipo a un puntatore di altro tipodynamic_cast
, fatto a runtimeSTATIC_CAST
// narrowing conversion
double d = 3.14;
int x = static_cast<int>(d);
// T* => void*
void* p; p=&d;
// conversione di void*
double* q = static_cast<double*>(p);
CONST_CAST Permette di convertire un puntatore o un riferimento da const T a T. Attenzione: quando si usa, probabilmente c’è un errore di design.
const int i=5;
int* p = const_cast<int*>(&i);
void F(const C& x) {
x.metodoCostante();
const_cast<C&>(x).metodoNonCostante();
}
REINTERPRET_CAST Permette di convertire il puntatore di un tipo a un puntatore di un altro tipo.
Classe c;
int* p = reinterpret_cast<int*>(&c);
const char* a = reinterpret_cast<const char*>(&c);
string s(a);
cout << s;
Per funzioni uguali tra tipi differenti, senza il template, dobbiamo fare tante funzioni quanti i tipi che abbiamo.
Per sistemare questo potremmo usare macro MA nel caso di operazioni negli argomenti, porta ad errori.
Programmazione generica
I template di C++ permette di operare funzioni e classi con tipo generico.
template <class T> // oppure template <typename T>
T min(T a, T b) {
return a < b ? a : b;
}
Il compilatore è fortemente tipato, questo codice non è compilabile. Deve essere istanziato.
int main() {
int i,j,k;
orario r,s,t;
...
// istanziazione implicita del template
k = min(i,j);
t = min(r,s);
// oppure: istanziazione esplicita del template
k = min<int>(i,j);
t = min<orario>(r,s);
}
I parametri di un template posso essere
L’istanziazione dei parametri di tipo deve essere UNIVOCA. L’uso di funzione template istanziata implicitamente su argomenti di tipo diverso, non compila.
Ci sono solo 4 conversioni implicite considerate:
int main() { int i=6; int& x=i; int a[3]={4,2,9}; E(x), F(e), G(i), E(7); }
L’uso di una funzione template istanziata esplicitamente su argomenti di tipo diverso, viene eseguita una conversione implicita.
``` c++
template <class T, int size> T min(T (&a) [size]) {
. . .
};
int main() {
int ia[20];
orario ta[50];
. . .
cout << min(ia);
cout << min(ta);
}
Qual è il modello di compilazione del template?
Compilazione per inclusione Il template completo (dichiarazione, definizione) è nel “header file”. Non c’è concetto di compilazione separata di un template. Problema 1: No information hiding. Problema 2: Istanza multiple del template per ogni uso in file diversi.
Problema 2 è risolvibile con dichiarazione esplicita di istanziazione
#include "min.h"
template int min(int, int);
template orario min(orario, orario);
E nella console:
g++ -fno-implicit-templates -c file.cpp
Esisteva una compilazione per separazione (export) per cui si compilava solo quello che si riusciva a compilare. Era difficile da implementare: stata rimossa da C++11.
La keyword export
è ora stata riutilizzata per la definizione di moduli.
Svantaggi dei template
Queue, coda FIFO (First In First Out)
// file queue.hpp
template <class T>
class QueueItem {
public:
QueueItem(const T&);
T info;
QueueItem* next;
};
template <class T>
class Queue {
public:
Queue();
~Queue();
bool empty() const;
void add(const T&);
T remove();
private:
static int contatore;
QueueItem<T>* primo;
QueueItem<T>* ultimo;
};
L’istanziazione di template di classe devono essere esplicite o devo definire un default.
template <class Tipo=int, unsigned int size=1024>
Nota bene: il compilatore genera una istanza di un template (di classe o funzione) solo quando è necessario. Istanzia solo le classi/funzioni richieste dal codice per compilare.
Anche se tutto il template sta nel “header” file, i metodo devono essere definiti fuori dalla definizione di classe. I campi dati statici sono istanziati fuori dal template di classe perché dipendono dal tipo del template.
// sempre file Queue.hpp
template <class T>
class QueueItem { . . . };
template <class T>
class Queue { . . . };
// definizione esterna
template <class T>
int Queue<T>::contatore = 0; // campo dati statico
template <class T>
QueueItem<T>::QueueItem(const T& val) : info(val), next(0) {}
template <class T>
Queue<T>::Queue() : primo(0), ultimo(0) {}
template <class T>
bool Queue<T>::empty() const {
return (primo==0);
}
template <class T>
void Queue<T>::add(const T& val) {
if(empty())
primo = ultimo = new QueueItem<T>(val);
else {
ultimo->next = new QueueItem<T>(val);
ultimo = ultimo->next;
}
}
3 tipi di amicizie
Friend associato
template <class U1,…,class Uk> class A;
template <class V1,…,class Vj> void fun(…);
template <class T1,…,class Tn> class C {
friend class A<…,Ti,…>;
friend void fun<…,Ti,…>(…);
}
Friend non associato Non molto usato, da amicizia completa
template <class T>
class C {
template <class Tp> // amicizia di metodo
friend int A<Tp>::fun();
template <class Tp> // amicizia di classe
friend class B;
template <class Tp>
friend bool test(C<Tp>); // amicizia di funzione
}
template <class T>
class Queue {
private:
// template di classe annidato “associato”
class QueueItem {
public:
QueueItem(Const T& val);
T info;
QueueItem* next;
};
. . .
};
Un QueueItem<T>
è un tipo implicito (typename) perché non è del tutto definito ma dipende implicitamente dai parametri di tipo di Queue<T>
.
template <class T>
class C {
public:
class X {
public: // template di classe annidata associato
T d; // T del template contenitore
};
template <class U>
class E { // template di classe annidata
public: // non associato
T x; // T del template contenitore
U y; // suo parametro di tipo U
void fun1() { return; }
};
template <class U>
void fun2() { // template di metodo istanza
T x; U y; return; // non associato
};
};
// ora voglio usare questi template per costruire una funzione
template <class T>
void templateFun(typename C<T>::D d) {
// C<T> usa un tipo che dipende da T
typename C<T>::X d2=d;
// E<int> usa template di classe annidata che dipende da T
// C<T>::E<int> usa un tipo che dipende da T
typename C<T>::template E<int> e;
e.fun1();
// c.fun2<int> usa un template di funzione che dipende da T
C<T> c;
c.template fun2<int>;
}
Per forward declaration, controlla il tuo compilatore.
Da non confondere con il C++ Standard Library, la libreria dei template di contenitori.
Contiene
Sequence Containers | Description | Random access |
---|---|---|
array | array class | x |
vector | vector | x |
deque | double ended queue | |
forward_list | linked list | |
list | double-linked list |
Associative Containers | Description |
---|---|
set | |
alberi |
for_each(InputIterator first, InputIterator last, UnaryFunction)
funzioni passati come argomenti
Array dinamico.
capacity() da lo spazio allocato per il vettore.
size() da il numero di elementi salvati nel vettore.
begin() e end() danno il puntatore per l’inizio e il successivo alla fine degli elementi salvati.
Ci sono 2 modi di usare vector: come l’array di C e lo stile STL del C++.
vector<int> v(10,-1) // vettore di 10 elementi da -1
// METODI CONTENITORI SEQUENZIALI
void push_back(const T&);
void pop_back(); // invoca distruttore
T& front();
T& back();
iterator begin();
iterator end();
Per fare input e output con un file, dobbiamo creare un oggetto ifstream.
Per scorrere il vettore, si deve fare uso di iteratori
C::iterator
, lettura e scritturaC::const_iterator
, sola lettura
begin() e end() ritornano const_iterator o iterator dipendentemente da se vettore è const o no.template <class InputIterator>
vector(InputIterator, InputIterator)
...
int a[20];
vector<int> v(&a,&a+6);
Metodi di cancellazione
vector<int> v{1,2,3,4};
v.pop_back(); // distrugge l'ultimo elemento
v.erase(2); // distrugge elemento in posizione 2, costo lineare
// funziona su contenitore qualsiasi
operator[] funziona solo con contenitore a accesso casuale
stack<deque>
e queue<deque>
List Implementata come una lista doppiamente linkata.
operator[]
non funziona)Classe che eredita da un’altra classe, si dice derivata (sottoclasse) -> D Classe da cui viene derivata si chiama classe base (superclasse) -> B
Classe base | Classe derivata con derivazione | ||
membro | pubblica | protetta | privata |
privato | inaccess. | inaccess. | inaccess. |
protetto | protetto | protetto | privata |
pubblico | pubblico | protetto | privato |