Home ] Su ] Novità ] FAQ ] Hardware ] Software ] Windows ] Sicurezza ] Linguaggi ] Cerca ] Contatta ] Guestbook ] Mappa ] Info su... ]

Le FAQ sul linguaggio C++ - Pagina 2


La versione originale di questa FAQ è sempre disponibile all'indirizzo http://www.penguinpowered.com/~The_Cpp_Compass/.
Nel suddetto indirizzo potete trovare materiale più aggiornato rispetto al contenuto della FAQ sul linguaggio C++ di questo sito.
Prima di continuare a leggere le seguenti sezioni della FAQ sul linguaggio C++, vi invitiamo a prendere visione del disclaimer, dei copyright e delle note di revisione, situate in questa pagina.





Le Frequently Asked Questions di it.comp.lang.c++ a cura di Roberto Gerola


Classi

Cosa sono le classi e quali sono le principali caratteristiche del linguaggio C++ ?

Una "classe" C++ e` un TIPO (astratto o concreto), che puo` raggruppare astrazioni di IDENTITA`, STATO, e COMPORTAMENTO (ciascuna di queste astrazioni potrebbe non essere presente in una data classe); una classe concreta permette di avere delle ISTANZE, dette OGGETTI "appartenenti" a quella classe (cioè, a quel tipo); una classe astratta permette di avere dei RIFERIMENTI ad istanze di opportune classi concrete che soddisfano la relazione di E'-UN ("is-A") rispetto alla classe astratta. Una data classe può essere simultaneamente concreta ed astratta (per questo si denominano anche "classi astratte PURE" quelle che concrete non sono, cioè che non permettono di avere istanze ma solo riferimenti).

Il C++ offre vari altri strumenti per definire tipi che classi non sono, ed è anche possibile usare una classe in un modo che NON definisce un tipo (uso abbastanza frequente in codice C++ antico, prima che nel linguaggio si aggiungesse il concetto di "spazio-dei-nomi"). Una classe può inoltre essere parametrizzata ("generica") rispetto a parametri-tipo, e/o a parametri-costanti, col meccanismo dei "template".

Una classe C++ permette di definire in modo articolato la accessibilità differenziata alle sue "feature" (dette anche suoi "membri"): alcune di esse possono essere riservate "per uso interno" (accessibili solo da parte dell'implementazione della classe stessa, ovvero "private"), altre possono essere "pubbliche", ed esiste una terza categoria intermedia, le feature "protette", comoda per l'implementazione della relazione di E'-UN attraverso il meccanismo della "eredità" (una classe C++ può "ereditare" da zero o piu` altre classi -- in modo pubblico, privato, o protetto, e, considerazione separata, in modo concreto o virtuale -- e questo ha tutta una serie di implicazioni relative alla classe stessa e all'uso che si può fare di riferimenti a sue istanze).

Gli elementi fondamentali della programmazione in C++ in ordine decrescente di importanza sono:
  1. C++ è un linguaggio MULTI-PARADIGMA: supporta, cioè, molteplici approcci diversi, e ortogonali fra loro, all'impostazione di un programma.
    Questo lo rende spaventosamente potente, e spaventosamente complicato. Un programma C++, quasi sempre, utilizza molteplici paradigmi (non necessariamente tutti quelli che il linguaggio supporta, naturalmente, sono usati in ogni singolo programma). Il "filo rosso" che unisce fra loro tutti i molteplici paradigmi può essere considerato quello della "correttezza dei tipi": il linguaggio permette al programmatore di violare esplicitamente la correttezza dei tipi, ma fa del suo meglio per consentire anche di rispettarla, coi due benefici di "controlli a tempo di compilazione" e "efficienza massimale a tempo di esecuzione".

  2. Paradigma procedurale: un programma C++ può fare pieno uso dei concetti tipici dei linguaggi procedurali (funzioni, variabili, espressioni, istruzioni, strutture di controllo, ...); il sottoinsieme procedurale del C++ è praticamente coincidente con il linguaggio C, con un minimo di cose in più relativamente alla correttezza dei tipi.

  3. Paradigma "uso di libreria standard": il C++ mette a disposizione dei programmi una libreria standard, molto potente e ottimizzata, per un certo insieme di funzionalità (che NON comprendono alcune cose importantissime, come la grafica, l'accesso a database, l'accesso alla rete, e tante altre interazioni col sistema): tipi di dati (ad esempio, stringhe di caratteri, numeri complessi, array-computazionali), contenitori parametrizzati (vettori, liste, code, insiemi, mappe), algoritmi, strutture di input/output formattato. Questa libreria standard non è "vasta" (rispetto ad esempio a quella di Java), cioè non copre moltissime aree di grande importanza, ma, per le aree che copre, offre una profondità (completezza, riusabilità, estensibilità) veramente inusitate. Quasi tutti i linguaggi offrono "qualcosa" del genere (su scala più ridotta), ma la specificità del C++ e` che tutto quanto sub 2 è implementato nel linguaggio stesso usando gli altri paradigmi qui elencati, e quindi può, in modo veramente "totale", essere esteso ed arricchito. L'uso assai vasto di parametrizzazioni (template) nella libreria standard consente di ottenere queste ricche funzionalità nel pieno rispetto della correttezza dei tipi.

  4. Paradigma "estensioni fuoribordo": il C++ permette di interfacciare con grande facilità librerie "esterne" al linguaggio (il termine di "fuoribordo" rende bene l'idea), più o meno standardizzate, per l'accesso a tutte quelle funzioni che [3] non copre -- database relazionali, reti, grafica, matematica avanzata, interazioni col sistema, eccetera.

  5. Paradigma "ad oggetti": il C++ permette di fare pieno uso del paradigma ad oggetti (polimorfismo) -- per ragioni storiche, questo paradigma ha colpito molti osservatori al punto che lo piazzerebbero "al primo posto" nell'elenco degli "elementi fondamentali", ignorando il fatto che [1..4] sono più importanti, e [6] quasi altrettanto importante. La programmazione ad oggetti in C++ e` ottenuta nel pieno rispetto della correttezza dei tipi.

  6. Paradigma "generico": il C++ permette di fare pieno uso del paradigma "generic programming" (programmazione parametrizzata) -- la libreria standard [cfr 3], ad esempio, si basa su [6] ancora più di quanto non si basi su [5] (anche [2] è cruciale, naturalmente, e [4] gioca il suo ruolo, il che, tutto assieme, dimostra che [1] è proprio IL punto cruciale. L'approccio C++ alla genericità è legato alla correttezza dei tipi e va nella stessa direzione (fare tutto il possibile a tempo di compilazione, non a tempo di esecuzione, per garantire un buon livello di controllo degli errori "anticipato", E l'efficienza ottimale a tempo di esecuzione).

  7. Paradigma "tipi di dati astratti": molti ignorano la potenza intrinseca di ADT, e sussumerebbero [7] sub [5], ma sarebbe altrettanto sbagliato che, ad esempio, sussumere [6] sub [2]. C++ contiene vari elementi che con [5] c'entrano sino ad un certo punto (Java, ad esempio, per cui la programmazione ad oggetti E` veramente centrale, ne manca), come la definibilità di assegnazioni e copie, che sono nel linguaggio proprio per supportare [7] in modo più ricco e completo (nell'ottica, manco a dirlo, della correttezza dei tipi.

  8. Paradigma "dinamico": C++ ha un supporto "minimale" per gli approcci dinamici, noto normalmente come "identificazione dei tipi a tempo di esecuzione" (RTTI: run-time type identification), che consente di implementare (alla [4]...) strutture dinamiche più ricche minimizzando le (inevitabili) violazioni della correttezza dei tipi implicite in questo dinamismo.

  9. Varie ed eventuali: il C++ è pieno zeppo di piccoli dettagli, da non trascurare, la maggior parte dei quali può essere riportata ai vari tipi di funzionalità previsti da [1..8] (alcuni, in realtà, hanno ragioni d'esistere puramente "storiche", ma anche essi contribuiscono fortemente, grazie alla compatibilità col passato che garantiscono, a supportare, in particolare, [4]).

Torna all'indice


Perchè il qualificatore di visibilità protected ?

Il qualificatore protected viene utilizzato per riservare l'accesso ad alcune feature di una classe X, alle sole classi che da X ereditano (uso "feature", come fa B. Meyer, per indicare "membri accessibili di classe o di istanza": statici o meno, dati o funzioni [metodi]).
Una classe D che eredita da una classe B ha per sua natura un grado di accoppiamento con B maggiore di quello di un'altra classe C che si limita ad "usarla" (per contenimento, ecc). [Per inciso, questo "maggior accoppiamento" è proprio la ragione per cui l'eredità va usata "quando serve", e non "tanto perchè c'è": minimizzare l'accoppiamento (coupling) fra parti del nostro software è uno dei vari metodi per migliorarne la qualitè].
Se "class D: public B", allora D *E`-UN* B, e questo comporta coupling elevato su vari piani (ad esempio, quello lessicale dello "scoping" entro i metodi della classe D).
E` dunque appropriato potere esprimere "l'accesso a queste feature comporta un grado di accoppiamento elevato", riservandole dunque agli "eredi" e non al "pubblico in generale". Se non ci fosse protected, tali "feature ad elevato coupling" avrebbero dovuto essere dichiarate public, nascondendo la distinzione.
Stroustrup discute a fondo, e lucidamente, la gran parte delle scelte di progetto del C++, e le loro evoluzioni storiche, nel suo ottimo testo "Design and Evolution of the C++ Programming Language", Addison-Wesley. In particolare, vi si può anche trovare la spiegazione più dettagliata dell'evoluzione di protected (compreso il tocco di rimpianto per l'esistenza, ormai ahimè irrevocabile, di dati protected, che, almeno così pensa ora Stroustrup, riflettono normalmente un coupling eccessivo; un protected applicabile a soli metodi potrebbe essere progettualmente preferibile).

Torna all'indice


Quando conviene usare una classe base virtuale ?

Sostanzialmente quando, in eredità multipla, si vuole una "identità" unica fra classi che se no sarebbero disparate.
Ecco un semplice esempio didattico basato sull'idioma "mix-in".
Supponiamo di avere una classe base astratta, tipo:

class emittore {
public:
 virtual void emetti(const std::string&) = 0;
};

le implementazioni di questa incapsulano il concetto di "emettere una riga" (magari la scrivono con iostream, magari la mandano ad un log di sistema, e/o la mostrano in una finestra su schermo, e così via).

Il modo più normale per connettere del codice "cliente" alla implementazione concreta di questa classe è passare al cliente un puntatore a emittore, a cui delegherà in modo esplicito l'output:

class chiaccherone {
 emittore* pEm;
public:
 chiacchierone(emittore* pEm): pEm(pEm) {}
 void parla() {
  pEm->emetti("Ciao!");
  // 998 linee omesse
  pEm->emetti("OK, basta.");
 }
};

da usarsi ad esempio con:
// si suppone: class emittore_su_stream: public emittore ...
emittore_su_stream ess(std::cout);
chiacchierone ciac(&ess);
ciac.parla();

Supponiamo, però, che per qualche buona e valida ragione scegliamo invece di fare la connessione con l'uso dell'eredità multipla.

Allora, dovremo usare emittore come base virtuale:

class emittore_su_stream: public virtual emittore { // ecc ecc
class chiacchierone_2: public virtual emittore {
public:
 void parla() {
  emetti("Ciao!");
  // 998 linee omesse
  emetti("OK, basta.");
 }
};

class cliente_connettore: public emittore_su_stream
  , public chiacchierone_2 {
public: cliente_connettore(std::iostream& x): emittore_su_stream(x) {}
};

Adesso, l'uso diverrà:
cliente_connettore x(std::cout);
x.parla();

Come vedi, l'eredità multipla (con base virtuale) ottiene risultati analoghi alla connessione esplicita ma con una fusione di identità fra 'cliente' dell'emittore (il 'chiacchierone') e 'servente' della stessa base astratta (l'emittore_su_stream).
Normalmente non è una gran bella alternativa perchè si riduce la flessibilità, ma a volte è proprio questo "fissaggio compile-time" quello che desideriamo (soprattutto se vogliamo parametrizzarlo in un template).

Che la base virtuale sia una base astratta pura (una "interfaccia", o come alcuni anche dicono [secondo me falsando un poco la terminologia più usuale] una "classe-protocollo") è molto tipico; è possibile costruire esempi in cui ha senso avere una base virtuale che non e` puramente astratta (sostanzialmente per una scelta di centralizzare uno "stato" -- ma normalmente, questo "stato" se ne starebbe meglio in una classe mixin, con i suoi accessori nella base astratta pura che fa da base virtuale), ma si tratta, così a ocio, di meno del 5-10% dei casi reali di uso di questo costrutto già un pò astruso.

Torna all'indice


Che cos'è l'eredità virtuale ?

"Concettualmente" (anche se oggi, in pratica, l'implementazione dell'eredità virtuale è fatta con maggior efficienza usando la vtbl), si immagini che ogni classe che ha una base virtuale, invece di una "copia" dei campi-dati di quella base al proprio interno, abbia entro di sè un "puntatore" a un oggetto di quel tipo.
Se quel tipo entra come base virtuale più volte in una eredità multipla, i vari "punti" in cui viene visto come base hanno tutti puntatori allo stesso oggetto di quel tipo; così, appunto come scrive Lippmann, c'è una singola istanza della classe base ("sub-oggetto") per quante volte possa apparire nella gerarchia di eredità multipla.
Un oggetto può facilmente trovare la sua base virtuale (basta seguire il puntatore), ma il percorso inverso non è così facile -- i cast alla C, e static_cast, non sono in grado di farlo; se hai la necessita` di "cast all'indietro" dalla base virtuale a una classe derivata, bisogna farlo sfruttando la run-time type identification, e specificamente con dynamic_cast.

Torna all'indice


Una classe che implementa un tipo di dato intero.

class Intero
{
 typedef unsigned char cifra;
 bool negativo;
 typedef std::vector<cifra> cont;
 cont cifre; // little-endian x comodità
 cifra cif(unsigned int N) { return N&0xFF; }
 void shifta(unsigned int& N) { N >>= 8; }
public:

 Intero(long L=0): negativo(L<0)
 {
  L=abs(L);
  while(L)
  {
   cifre.push_back(cif(L));
   shifta(L);
  }
 }

// stampa esadecimale big-endian:
 void emitHex(std::ostream& o) const
 {
  using std::ios_base;
  ios_base::fmtflags f = o.setf(ios_base:: hex, ios_base:: basefield);
  cont::const_reverse_iterator cri;
  for(cri=cifre.rbegin(); cri!=cifre.rend(); ++cri)
   o<<*cri;
  o.setf(f, ios_base::basefield);
 }

// meno unario (cambio di segno)
 Intero operator-() const
 {
  Intero result(*this);
  result.negativo = !negativo;
  return result;
 }

// NB: naturalmente non ha senso fare questo
// inline, lo scrivo qui solo x semplicità:

 Intero& operator+=(const Intero& a)
 {
  f(a.negativo!=negativo)
  return operator-=(-a);
  cont::cost_iterator ci = a.cifre.begin();
  cont::iterator i = cifre.begin();
  unsigned int riporto = 0;
  while(i!=cifre.end() && (riporto || (ci!=a.cifre.end())))
  {
   if(ci!=a.cifre.end())
    riporto += *ci++;
   riporto += *i;
   *i++ = cif(riporto);
   shifta(riporto);
  }
  while(riporto || (ci!=a.cifre.end()))
  {
   if(ci!=a.cifre.end())
    riporto += *ci++;
   cifre.push_back(cif(riporto));
   shifta(riporto);
  }
  return *this;
 }

// ecc ecc x gli altri operatori aritmetici + interessanti! };

// operatori ausiliari x comodità:

inline ostream& operator<<(ostream &o, const Intero& i)
{
 i.emitHex(o); return o;
}

inline Intero operator+(const Intero&a, const Intero&b)
{
 Intero result(a);
 result += b;
 return result;
}

// eccetera

Le varie operazioni intere possono essere definite con algoritmi che bene o male abbiamo imparato alle elementari (con la piccola differenza che qui si lavora in base 256 invece che in base 10, ma nulla impedisce, volendo, di lavorare invece in base 10, o 100, per avere comodamente I/O decimale a prezzo di operazioni un poco più lente; basta ridefinire opportunamente cif e shifta con uso degli operatori % e / fra interi).

Se interessa gestire numeri non interi, si possono scegliere per essi varie rappresentazioni (basate magari sulla classe Intero): ad esempio, frazioni (presumibilmente da mantenersi ridotte ai minimi termini, con una coppia di Intero come numeratore e denominatore), numeri a "virgola fissa" (parte intera e parte frazionaria, ma quest'ultima dovrà allora avere precisione prefissata e non illimitata), significando+esponente (il primo un Intero, per il secondo, soprattuto in base 256, potrebbe anche bastare un unsigned long, comunque si può sempre usare un Intero anche lì...). L'onere computazionale delle varie operazioni elementari, e la difficoltà di scriverle, varia, come è ovvio, a seconda della rappresentazione scelta.

Torna all'indice


Cos'è il costruttore di copia ?

Il copy constructor è quello che viene richiamato per instanziare un oggetto a partire da un altro oggetto della stessa classe.
Ad esempio :

string stringa1("Goofy"); //ctor

string stringa2(s1); //copy ctor

A questo punto anche stringa2 contiene "Goofy" ed è stato costruito "e;copiando"e; l'istanza stringa1. Se il copy ctor non è definito esplicitamante, il default stabilito dal linguaggio consiste nel generare una copia bit a bit; questo comporta gravi problemi se viene allocata dinamicamente memoria.
Ad esempio :

class c {
 private:
  double *_pd;
 public:
  c(const double d = 0.0) throw(bad_alloc)
  {
   _pd = new double(d);
  };
  ~c() throw()
  {
   delete _pd;
  };
};

Un' istruzione c c1(5.0); alloca memoria con operator new, ed il pointer alla memoria allocata è conservato in _pd. Una successiva c c2(c1); creerebbe una copia bit a bit di c1 e la metterebbe in c2: il _pd dell'istanza c2 punta allora alla stessa memoria allocata da c1. Quando c1 viene distrutto, la memoria puntata da _pd viene deallocata; e se anche c2 viene distrutto, si tenta di deallocare nuovamente la stessa memoria e si ottiene un errore a run-time. Oppure, se c2 veien distrutto, c1 contiene un pointer a memoria deallocata: e usando c1 si ottiene nuovamente un errore a run time.

In casi come questo (operator new nel ctor) bisogna sempre definire il destructor (con operator delete), il copy constructor e l'assignment operator (che ha gli stessi problemi).
Ad esempio :

...
 public:
...
  c(const c & rhs) throw(bad_alloc)
  {
   _pd = new double(*(rhs._pd));
  };
  c & operator = (const c & rhs) throw(bad_alloc)
  {
   if (this != &rhs)
   {
    delete _pd;
    _pd = new double(*(rhs._pd));
  }
  return *this;
...

Attenzione che un copy ctor può essere invocato di nascosto; se ad esempio si passa ad un metodo un oggetto della classe c per valore, viene creata una copia di quell'oggetto da usare al posto del dummy argument.

Torna all'indice


Eccezioni

Cosa significa la keyword throw nella definizione di una funzione ?

throw() oppure throw(exception list...) nella dichiarazione di una funzione indica quali eccezioni possono essere lanciate da quella funzione. throw() senza niente fra le parentesi vuol dire che quella funzione non lancia eccezioni. Nessuna dichiarazione throw(...) significa che la funzione puo' lanciare qualsiasi eccezione.

Torna all'indice


Files

Come cancellare e rinominare un file ?

Per cancellare un file (non attualmente aperto da parte di alcun programma) di cui si conosce la path, si può usare la funzione

 int remove(const char* path)

definita in <stdio.h>: si passa la path, torna 0 in caso di successo (se fallisce, torna non-0 e setta errno).
Per cambiare di nome ad un file (stesse condizioni), si può usare la funzione

 int rename(const char *oldname, const char *newname)

sempre da <stdio.h>.

Queste sono funzioni che esistevano già nello Standard ISO per il C, che quello del C++ "incorpora" interamente; lo Standard C++ non ha aggiunto altri modi di ottenere gli stessi risultati.

Torna all'indice


Libreria standard e containers

Come gestire una lista di oggetti costantemente ordinata ?

L'approccio più semplice è usare std::set se non possono mai essere in lista due elementi equivalenti (cioè tali che, per l'ordinamento debole D usato, x D y è falso e y D x è pur esso falso). Altrimenti si può ricorrere a std::multiset in quanto, in generale, non si può escludere che interessi anche tenere nel contenitore elementi fra loro equivalenti. In questo caso e` da notare che l'ordinamento imposto da multiset NON è "stabile" (elementi equivalenti possono trovarsi in qualsiasi ordine fra loro), quindi può darsi che convenga in ogni caso (se interessa la "stabilità") usare un set, arricchendo gli elementi di un identificatore unico (ad esempio, l'ordine progressivo di arrivo nel contenitore) in modo da escludere l'equivalenza.

E l'utilizzo della funzione sort della libreria standard ?
Normalmente list<>::sort è una accurata implementazione di mergesort, mentre l'algoritmo sort (non adatto a list<>, bensì a contenitori come vector<>, con iteratori random-access) usa l'eccellente algoritmo di Singleton (dal cognome del suo autore, nulla a che vedere con la pattern detta "Singleton"). L'importante è la garanzia che sort è O(N logN) anche nel caso peggiore (qsort, invece, garantisce O(N logN) solo come media, ed è O(N quadrato) nel caso peggiore). Richiamare l'algoritmo di ordinamento ad ogni inserimento è troppo oneroso in quanto la somma per i da 1 a N di i log(i) è più che quadratico in N, quindi costruire una lista di N elementi con una chiamata a sort ad ogni elemento inserito avrebbe un costo assolutamente disastroso.

Esiste un altro metodo ?
Sì, c'è un approccio generale, decisamente molto sofisticato, basato sull'osservazione che, nel 99% dei casi in cui un analista ingenuo potrebbe pensare che un dato contenitore "deve essere sortato in qualsiasi momento", esso, in realtà, deve esserlo solo in certi specifici momenti di "osservabilità" -- i momenti in cui quel contenitore viene "percorso" (e deve fornire i suoi elementi in quel momento nel giusto ordine)ovvero "frugato" (ricerca di un elemento, ad esempio con binary_search). A questo si unisce il fatto che, nel mondo reale, molto spesso i contenitori "alternano" fasi di "inserimento", in cui la grande maggioranza delle operazioni sono appunto inserimenti di nuovi elementi (o, in certi casi, cancellazione o alterazione di elementi esistenti), e fasi di "percorso/fruga", in cui la grande maggioranza delle operazioni non alterano il contenitore. I "contenitori bifasici" sono basati sull'idea di trarre tutto il vantaggio da questo tipo di "alternanza"; per chiarire, un modello semplificato della completa pattern "contenitore bifasico" potrebbe essere semplicemente:
  • se arrivano nuovi elementi o altri "editing" (alterazioni/cancellazioni), entrare in "fase di alterazione" se già il contenitore non vi si trovava, e non apportare modifiche al "corpo principale del contenitore" (che è sortato), bensì accumulare le richieste di editing in un buffer separato
  • se arriva una richiesta di "percorso" o "fruga", entrare in "fase di percorso" se già il contenitore non vi era, e quindi apportare tutti gli editing "pending" ripristinando il "corpo principale" del contenitore prima di concedere l'accesso "di fruga" (percorso o ricerca che sia)
Con questo approccio semplificato, la prima richiesta "di fruga" dopo una sequenza di richieste "di editing" può subire un notevole rallentamento, anche se il "costo ammortizzato" resta potenzialmente molto buono;fra i raffinamenti che portano da questo approccio più semplice, a quello "completo" del pattern, vi è lo sfruttamento delle caratteristiche delle "heap" per ammortizzare più uniformemente il costo (migliora il ritardo di caso peggiore, con un lieve appesantimento del ritardo medio e quindi lieve peggioramento del throughput); inoltre, questo raffinamento permette di "irrobustire" il passaggio di fase ("introdurre una isteresi"), così che una occasionale richiesta "di fruga" ad un contenitore "in fase di editing", o viceversa, non forza ancora la commutazione di fase, bensì può venire soddisfatta pur essendo "nella fase sbagliata".
Naturalmente, questa enorme complessità implementativa va giustificata nei fatti, verificando, anzitutto, che un approccio più semplice ed elementare non sia già del tutto soddisfacente.
Come si diceva più sopra l'approccio più semplice in assoluto è usare un multiset (std::multiset, naturalmente): questo è mantenuto in ordine ad ogni insert (ogni insert costa log K, quindi il costo di mantenimento è l'"imbattibile in astratto" N log N,o meglio somma per i da 1 a N di log N, leggermente inferiore ma asintoticamente equivalente), esattamente come da specifiche. Qualsiasi approccio più sofisticato puo` battere questo se e solo se e` in grado di trarre vantaggio, anche solo "euristicamente", da caratteristiche di non-ergodicità degli inserimenti (v. sopra ad esempio il concetto di "fasi"), o da altri trucchi (radix sort, ecc).
[A volte, inoltre, il semplice fatto di poter passare a multiset::insert un iteratore di "hint", se si ha già una idea di dove probabilmente si inserirà il nuovo elemento,può bastare per battere NlogN con un multiset: infatti, se "ci prendo sempre" con l'iteratore che passo, allora il costo di inserimento è ammortizzato costante, invece che log K...! Naturalmente, se l'iteratore di "hint" è "sbagliato", il tempo rischia di peggiorare, invece che di migliorare]

Torna all'indice


Quali sono i contenitori standard che il C++ mette a disposizione ?

list, vector, map, set, deque, multiset, multimap , più casi particolari come string e valarray (e i container-adapter come stack, ma quelli non sono contenitori ma "vestiti" da mettere su contenitori), e infine, naturalmente, il costrutto di bassissimo livello "array builtin".
Popolari implementazioni come quella liberamente disponibile dalla SGI aggiungono vari altri contenitori, come slist, hash_map,hash_set, rope, eccetera, ma lo standard non li richiede.

Torna all'indice


Quali differenze ci sono tra list e set ?

Le differenze sono molteplici:
  • una list può avere ripetizioni, un set no;
  • percorrendo una list si deve ricevere gli elementi in ordine rigorosamente deducibile dalle operazioni di inserimento compiute;
  • percorrendo un set si deve ricevere gli elementi in ordine rigorosamente crescente rispetto al predicato di ordinamento fra loro che il set usa.
Di consequenza, per un set non avrebbero senso le operazioni di inserimento come push_back e push_front, e gli accessori come front e back, che una list deve avere, come pure mutatori come sort (il set è sempre "sortato"). Sempre di conseguenza, sono diverse tutte le varie specifiche di prestazioni -- sia in termini di O(), sia in quelli relativi a quali operazioni invalidano quali iteratori. Infine, per non alterare l'ordine, in un set non si può mutare gli elementi in-place (le reference che si ottengono dagli iteratori di un set sono a costanti).

Torna all'indice


Cos'è un map e che differenze ci sono tra set e multiset ?

Una map può essere considerato come l'equivalente "associativo" di un array, cioè un array dove gli indici sono di tipo del tutto arbitrario (l'uso di operator[] per l'indiciamento non prende però un tempo O(1), ma O(log N)); oppure puoi vederlo come una estensione di set, dove, a fronte di ogni elemento ("chiave") presente, vi è un "dato ad esso associato" ("valore" attualmente associato a quella chiave).

La differenza cruciale tra set e multiset è che l'inserimento di un elemento equivalente a uno già presente, in un multiset, avviene senza problemi (in un set, naturalmente, no, poichè il set non ammette duplicati); di conseguenza, la ricerca di un elemento in un set è una operazione "si/no" (o c'è, o non c'è), in un multiset torna un range di elementi equivalenti a quello cercato (range vuoto, se nessuno e` presente, ma, se non vuoto, può contenere un elemento, o due, o tre, o ... senza limiti).

Torna all'indice


Cosa sono gli iteratori ?

In generale un iteratore è una design pattern che può essere vista come astrazione di un puntatore: un oggetto al quale si può applicare "alcune" delle tipiche operazioni che si applicano ai puntatori. Quale esatto insieme di operazioni si applichi definisce la "categoria" di iteratore. Ad esempio, un iteratore "bidirezionale" puó essere dereferenziato (operator*, e se del caso [se indica una struct o class] anche operator->), e può essere incrementato (operator++, pre e postfisso) e decrementato (operator--, idem), ma non può invece essere soggetto ad arbitrarie operazioni (come +=n). Questo permette di usare "quasi come un puntatore" degli oggetti che "navigano" all'interno di strutture dati dove non hanno necessariamente senso tutte le operazioni di "navigazione" che un puntatore rende possibili all'interno di un array.

Torna all'indice


Come convertire un intero in una stringa ?

La soluzione piu` elegante e` usare gli string streams :

#include <sstream>
#include <string>

...

int aValue = 72;

std::ostringstream aOutStream;
aOutStream << "My int value: " << aValue <<
std::endl;

std::string aString = aOutStream.str();

Torna all'indice


 



Disclaimer

SourceNet non riconosce nessun tipo di garanzia per il contenuto di tutta la sezione dedicata alle FAQ sul linguaggio C++ pubblicata su questo sito (SourceNet Italia).
Tutto il contenuto della FAQ sul linguaggio C++ è fornito "così come è", senza alcuna garanzia di qualsiasi tipo, sia espressa che implicita, ivi incluse, senza limitazioni, le garanzie implicite di commerciabilità o idoneità per uno scopo particolare ovvero quelle che escludano la violazione di diritti altrui. L'intero rischio derivante dall'uso o dalle prestazioni del contenuto di tutta la sezione della FAQ sul linguaggio C++ rimane a carico dell'utente.


Copyright

Il contenuto della sezione delle FAQ sul linguaggio C++ situata in questo sito è la copia della FAQ sul linguaggio C++, pubblicata all'indirizzo http://www.penguinpowered.com/~The_Cpp_Compass/.
Il contenuto dell'intera FAQ sul linguaggio C++ è di proprietà di Roberto Gerola.
Viene concessa la possibilità di poter copiare il contenuto dell'intera FAQ sul linguaggio C++ di questo sito solo previa autorizzazione di Roberto Gerola (http://www.penguinpowered.com/~The_Cpp_Compass/).


Note sulla revisione

La versione di questa FAQ è aggiornata alla FAQ sul linguaggio C++, ultimo aggiornamento 24/08/1999, di Roberto Gerola (http://www.penguinpowered.com/~The_Cpp_Compass/).

 

Torna ad inizio pagina

[ Le FAQ sul linguaggio C++ - Pagina 2 ] Le FAQ sul linguaggio C++ - Pagina 3 ]

Ultimo aggiornamento : 17/01/2009.   
Home ] Su ] Novità ] FAQ ] Hardware ] Software ] Windows ] Sicurezza ] Linguaggi ] Cerca ] Contatta ] Guestbook ] Mappa ] Info su... ]

Copyright © 1997-2070, Joseph Parrello. Tutti i diritti sono riservati.

Siete il visitatore n. Contatore Sito
Bpath Contatore
dal 17 gennaio 2009.