Blog‎ > ‎Articoli‎ > ‎

Appunti C++

pubblicato 13 gen 2015, 00:30 da Rudy Azzan   [ aggiornato in data 17 mag 2017, 06:09 ]

Intervista all'esperto (un mio ex collega con molti anni di esperienza in C++)

Alcune domande

Devo adottare qualche convenzione di scrittura del codice particolare?
No, scrivi come meglio credi

Come creo un interfaccia o una classe astratta?
Per creare un interfaccia scrivi

virtual int mangia()=0

Se nella classe implementi un altro metodo la classe diventa astratta

Bad pratics? 
Mai riferimenti a oggetti interni alle classi

Best pratics?
Se ho un puntatore devo implementare il costruttore di copia privato e l'operator =

A cosa serve la parola chiave "Extern"? 
"Extern" dice che la funzione si trova da qualche parte 

Come si usa la parola chiave "static"?
Come in c#

Quando e perché si deve usare una "Struct"?
Solo per aggregare dati

Che differenza c'è a mettere gli operatori * o & prima o dopo una variabile?
E'uguale ma ci sono sono convenzioni e ideali dei programmatori

Quando è meglio usare lo heap o stack?
A seconda; nello stack viene svuotata quando esci dallo scope della funziona, nello heap devi arrangiarti tu a dealloccarla. In questo caso ti salvano gli Shared_ptr (include tr1/memory) della boost library

Come creo un Include globale?
Metti tutte le dichiarazioni in un unico file .h

A che server "Pragma once"?
Boh.. ma roba non usata

Che differenza c'è fra Include con virgolette o con parentesi angolata?
Con virgolette cerca prima nei miei files. Con parentesi angolata cerca prima nelle cartelle di sistema. Come regola metti sempre prima gli include di progetto poi quelli di sistema

Meglio usare e sono più prestanti le Inline function o i define?
Le funzioni inline sono automatiche e decise dal compliatore. I define meglio non usarli o solo per cose semplici tipo i log.  #p stampa il nome della variabile

Hai dei link utili? 

Dove meglio dichiarare degli include nei file .h o .cpp?
Meglio metterli solo dove serve

Quando mettere del codice nel headers (file .h) quando?
Quando ho massimo 3 righe non complsse. (Es. : swap)

In c# c'è la parola chiave "using namespace" e in C++?  
Anche in c++ c'è lo "using namespace" MA DEVI USARLO SOLO NEI FILE .cpp NON NEI .h

Che differenza c'è nel mettere la parola chiave "Const" prima o dopo la dichiarazione di una variabile?
Se lo metti prima di una variabile, indica che il valore è costante, se la metti dopo, vuol dire che la funzione non può in nessun modo variare il contenuto dell'oggetto

Come passare i parametri?
Se l'oggetto si fa una sua copia interna del parametro allora passalo per riferimento &, se l'oggetto deve usare lo stesso oggetto utilizzato da fuori passa il puntatore *.

A(const MyClass& p); mi aspetto che A faccia una copia interna sua di MyClass. Oppure usi il parametro passato in sola lettura, per consultazione. 
A(MyClass* p); mi aspetto che A utilizzi lo stesso oggetto p che passo in input (qui è da decidere CHI eliminerà l'oggetto p, o la classe A nel distruttore o il chiamante ... UNO dei due, non entrambi)

A(MyClass& p); mi aspetto che A effettui dei cambiamenti su MyClass, ma non lo memorizzi da nessuna parte.

namespace db {

void getcars(Collection& result) {
...
result.add(etc etc)
...
}

Collection* getcars() {
Collection* result = new Collection();
...
result->add(etc etc)
...
return result;
}

std::tr1::shared_ptr<Collection> getcars() {
std::tr1::shared_ptr<Collection> result(new Collection());
...
result->add(etc etc)
...
return result;
}


Collection getcars() {
Collection result;
...
result.add(etc etc)
...
return result;
}

}

Questo è solo con il namespace,
la prima versione è ok
la seconda richiede che il chiamante faccia la "delete(result)" quando ha finito (altrimenti c'è il memory leak)
la terza usa gli smartpointer
l'ultima fa una copia di tutto l'oggetto Collection (all'uscita)

Dichiarazioni Setter e Getter

//GETTER tipi semplici
int getData() const { .... }
//GETTER tipi complessi
const std::string& getData() const { return msLamiastringa; }
//SETTER tipi semplici
void setData(int value) { miValore = value; }
//SETTER tipi complessi
void setData(const std::string& value) { msValore = value; }

Analisi di un caso particolare

class A {
public:
    A() { pippo = new char(12); }
    //~A() { delete pippo;}
private:
    char* pippo;
}

cosa succede secondo te se scrivo:

A primo;
{ A secondo = primo; }

succede che "secondo" viene distrutto.
Viene chiamato il suo distruttore che libera la memoria a cui punta pippo, ma in "primo" pippo è ancora valorizzato e "primo" crede di avere ancora la memoria a disposizione e così hai il patatrac.
In questo caso bisogna implementare il distruttore per liberare la memoria (Basta decommentare la quarta riga).
Per essere completi meglio imparare la regola dei 3, che dice che devi implementare anche gli altri 2, è cioè il costruttore di copia e l'assegnamento (vediamo più avanti cosa significa).

In questo caso il costruttore di copia sarebbe una roba tipo

A(const A& other) {
    pippo = new char(12);
    memcpy(pippo, other.pippo, 12);
}

e uguale op assegnamento

La regola dei tre

in pratica la regola dice che:
Il "distruttore", il "costruttore di copia" e "l'operatore di assegnamento" QUANDO non vengono specificati dal programmatore vengono creati automaticamente dal compilatore.

SE per qualche motivo devi implementare uno di questi 3 metodi ALLORA devi implementare SEMPRE anche gli altri 2.

Il compilatore non si lamenta se non lo fai perché per lui il codice è ok ma è un errore di programmazione, ti faccio un esempio con questa classe:

class cTest {
    public:
        cTest() 
            : mPtr(new cMyClass())
        {
        }
    private:
        cMyClass* mPtr;
    };

come hai detto tu ieri bisogna implementare il distruttore per liberare la memoria:

~cTest {
    delete mPtr;
}

A QUESTO punto la REGOLA DEI TRE dice che devi implementare anche gli altri 2 metodi e ti spiego perché...
Cosa succede in questo codice?

{
    cTest a;
    {
        cTest b(a); // costruttore di copia
        cTest c;
        c = a;  // assegnamento
    }
    // COSA SUCCEDE QUI?!!?!?!?
}

in pratica succede che [b] e [c] vengono distrutti, viene chiamato il distruttore e questo libera il puntatore.
Nel punto segnato l'istanza [a] contiene un puntatore invalido.

Secondo la REGOLA DEI TRE allora devi aggiungere ANCHE questi 2 metodi (che risolvono il problema):

// costruttore di copia
cTest(const cTest& rhs) 
    : mPtr(new cMyClass(rhs))
{
}
    
// operatore di assegnamento
const cTest& operator=(const cTest& rhs) {
    if (this != &rhs) {
        cMyClass* tmpPtr = new cMyClass(rhs);
        delete mPtr;
        mPtr = tmpPtr;
    }
    return *this;
}

// assegnamento con swap
const cTest& operator=(const cTest& rhs) {
    cTest tmp(rsh);
    swap(temp);
    return *this;
}

// altra variante di assegnamento con swap
const cTest& operator=(cTest rhs) {
    swap(rhs);
    return *this;
}
N.B.: Il codice è stato scritto direttamente senza provarlo, quindi potrebbe non compilarsi.

Una nota su quest'ultimo metodo, per rendere questo metodo il più possibile "exception safe" si usa SEMPRE questa sequenza di operazioni:
  1. acquisire le nuove risorse
  2. rilasciare le vecchie risorse
  3. assegnare le nuove risorse all'oggetto

Creazione di oggetti non copiabili

/* 
==========================================================
cUncopyable.h
Questa classe ti permette di creare oggetti che non possono
essere copiati.
Il trucco è mettere privati:
    - il costruttore di copia
    - l'operatore di assegnamento 
==========================================================
*/
#ifndef RUDY_CUNCOPYABLE_H
#define RUDY_CUNCOPYABLE_H

namespace rudy {

class cUncopyable {
protected:
    cUncopyable() {}
    virtual ~cUncopyable() {}
    
private:
    cUncopyable(const cUncopyable&);  // disable copy constructor
    const cUncopyable& operator=(const cUncopyable&); // disable copy assignment operator
};

}

#endif  // RUDY_CUNCOPYABLE_H

Creazione di una classe singleton

/*
==========================================================
cMySingletonClass.h
Questa classe rappresenta un singleton.
Il modo di creare un singleton in C++ è questo:
    - impedire che la classe possa essere copiata
    - impedire che la classe possa essere istanziata dall'esterno
Quella che vedi qui:
    - non può essere copiata in quanto eredita da cUncopyable 
    - non può essere istanziata perché il costruttore è privato
L'unico modo per accedere all'UNICO oggetto disponibile è 
attraverso il metodo statico, ad esempio:
    std::cout << "id=" << cMySingletonClass::GetInstance().getID()
              << " "
              << "info=" << cMySingletonClass::GetInstance().getInfo()
              << std::endl;
==========================================================
*/

#ifndef RUDY_CMYSINGLETONCLASS_H
#define RUDY_CMYSINGLETONCLASS_H

#include "cUncopyable.h"
#include <string>

namespace rudy {

class cMySingletonClass : private cUncopyable {
public:
    // Questo metodo statico ti consente di accedere all'unica istanza
    static cMySingletonClass& GetInstance() {
        static cMySingletonClass mInstance; // questo è l'unico oggetto disponibile
        
        // NOTA: l'oggetto viene effettivamente creato in memoria alla prima chiamata del metodo, 
        // detto in altro modo, se nessuno chiama questo metodo l'oggetto non verrà mai creato
        
        return mInstance;
    }
    
    int getId() const {
        return mId;
    }
    
    const std::string& getInfo() const {
        return msInfo;
    }
    
private:
    int mId;
    std::string msInfo;

    // NOTA: il costruttore e il distruttore li metti privati così
    // l'oggetto non può essere istanziato dall'esterno

    cMySingletonClass() 
        : mId(42)
        , msInfo("Ciao io mi chiamo Gigi")
    {
    }

    ~cMySingletonClass() 
    {
    }
        
};

}

#endif  // RUDY_CMYSINGLETONCLASS_H

Note varie

Non tornare come puntatori oggetti dello stack (senza new) perché appena escono dallo scope vengono persi e si va in uno stato inconsistente

Liste d'inizializzazione

metodo(string _nome) : nome(_nome){
//… 
}

//Istanza vuota: 
string g(); //<- non inizializza
string g; <- ok

Varie sui puntatori

// in questa versione il chiamante DEVE ricordarsi di distruggere l'oggetto
image* acquisisci() {
    camera c;
    imagecv ii;
    c.read(ii);

    // in questo caso la funzione alloca lo spazio per l'oggetto
    image* i = new image();
    ii -> i
    return i;
}

void acquisisci(int a, int b, image* i = NULL) {
    camera c;
    imagecv ii;
    c.read(ii);
    insert ...

    // i può essere null (param opzionale)
    if (i 1= NULL) {
        ii -> i
    }
}

void acquisisci(image& i) {
    // i non può essere null
    camera c;
    imagecv ii;
    c.read(ii);
    ii -> i
}

image& acquisisci() {
    image* p = new image();
    return *p;
}

// Roberto userebbe questo!
shared_ptr<image> acquisisci() {
    camera c;
    imagecv ii;

    shared_ptr<image> i;
    if (c.read(ii)) {
        i.reset(new image());
        ii -> i
    }

    return i;
}

acquisisci(23,34);
acquisisci(23,34,i);

class test
{
public:
    test();
    ~test();

    bool validate() const {
        return true;
    }

private:
};



class gigi
{

public:
    gigi();
    ~gigi();

    gigi& incrementaValore() {
        i++;
        return *this;
    }

    gigi& decrementaValore() {
        i++;
        return *this;
    }

    gigi& setValore() {
        return *this;
    }

    gigi& setIe() {
        return *this;
    }

    const int getPippo/() const {
        return mPippo;
    }

    void setPippo(const altraClasse& value) {
        value.validate();
        mPippo = value;
    }

private:
    int i;
    /* data */
};

int main(int argc, const char *argv[])
{
    gigi g;
    g.setValore().setIe();
    return 0;
}


bool test(int a, int& out1, int* out2, gigi& out3, gigi** out4) {
    out1 = 23;
    if (out2 != NULL) {
       *out2 = 45;
    }
    out3 = gigi(333);
    if (out4 != NULL) {
       *out4 = new gigi(444);
    }

    // ... qui qualcosa va storto
}

int a = 12;
int out1;
int out2;
gigi out3;
gigi* out4 = NULL;

out3.gaga();

if (test(a, out1, &out2, out3, NULL)){
}

if (test(a, out1, &out2, out3, &out4)){
}

Versione di C++ in uso

std::cout << __cplusplus;
std::getchar();


Nota sulle costanti
 
int m1(11), m2(13);

cont int* n1(&m1);
n1 = &m2;

Il numero non varia, ma può variare l'indirizzo puntato. In questo caso se non deleto m1 è un leak, perché alla fine dello scope viene perso.

Regole varie

  • Se un metodo non modifica l'oggetto deve essere sempre const
  • Il pattern builder si usa per chiamare vari pezzi, e ogni uno fa il suo lavoro per ottenere un prodotto
  • Il factory chiama builder per creare oggetti, Es.: bici da donna, uomo ecc..
  • Prima di usare il puntatore allocare la memoria!
  • Chi genera la risorsa deve anche distruggerla!
  • Un interfaccia che richiede che il client si ricordi qualcosa (es. eliminare oggetto) è sbagliata
  • Per passare un tipo di dato corretto ad un interfaccia è meglio fate un tipo dedicato piuttosto che passare un valore potenzialmente sbagliato per poi controllarlo e dare errore
  • Per creare oggetti è meglio fare delle factory in modo che tornino sempre oggetti smart pointer e non sia l'utilizzatore a doversi sempre ricordare di faro

Schema d'uso delle costanti

const int x;      // constant int
x = 2;            // illegal - can't modify x

const int* pX;    // changeable pointer to constant int
*pX = 3;          // illegal -  can't use pX to modify an int
pX = &someOtherIntVar;      // legal - pX can point somewhere else

int* const pY;              // constant pointer to changeable int
*pY = 4;                    // legal - can use pY to modify an int
pY = &someOtherIntVar;      // illegal - can't make pY point anywhere else

const int* const pZ;        // const pointer to const int
*pZ = 5;                    // illegal - can't use pZ to modify an int
pZ = &someOtherIntVar;      // illegal - can't make pZ point anywhere else


Chiamata alla classe base

class SpecialWindow: public Window {
public:
    virtual void onResize(){
        Window::onResize();
        //...
    }
    //...
};

Classificazione delle librerie

Librerie statiche: Vengono inglobate nell'eseguibile
Librerie dinamiche: Sono file esterni che vengono caricati in memoria quando l'applicazione parte

Cattivo utilizzo di share pointer

//In questo caso non si sa in che ordine vengono pocessati i parametri e 
//se priority va in errore potrebbe verificarsi un memory leak
processaFinestra(boost::shared_ptr<Window>(new Window),priority());

//Questa è la forma corretta da preferire
boost::shared_ptr<Window> wp(new Window);
processaFinestra(wp,priority());
Comments