Introduzione al C++

Discussioni su: C/C++, Visual Basic, Delphi, HTML, Java, VBScript, PHP, SQL ...
Rispondi
LoryOne
Senior Member
Senior Member
Messaggi: 31
Iscritto il: 19/07/2018, 9:56
Stato: Non connesso

Introduzione al C++

Messaggio da LoryOne » 03/08/2018, 10:41

File stream: Copia da un sistema a 32 bit di un file pesante oltre 4Gb
Questa guida in C++, la prima nella sezione Sviluppo del grectech Forum, è rivolta ai principianti con una certa familiarità sui tipi di dato gestiti dal linguaggio C++, a coloro che si accingono a compiere i primi passi nell'utilizzo di questo ostico ma potente linguaggio, fornendo al contempo importanti indicazioni preliminari per un corretto approccio alla programmazione "con metodo" e che vede impiegate poche ma semplici regole dogma, troppo spesso sottovalutate in fase di progettazione, che possono portare a comportamenti imprevedibili ed, in alcuni casi, a dir poco disastrosi se non seguite con attenzione.
Non è intenzione dello scrivente fornire spiegazioni riguardo alle classi, all'ereditarietà delle stesse, all' overload o al polimorfismo di cui il linguaggio C++ è ricco quale potenziamento ulteriore del linguaggio C e rivolto alla così detta OOP (Programmazione Orientata agli Oggetti), poiché ritiene che esse non debbano essere note al lettore in questa specifica guida e comunque materia d'integrazione riguardo alle conoscenze di C fin'ora acquisite, necessarie a compredere a pieno le innovazioni apportate dal suffisso '++' che determina il nome del linguaggio trattato.
Pertanto, la guida si prefigge tre scopi essenzialmente:
  • Il primo è quello di fornire una porzione di codice riutilizzabile;
  • Il secondo è quello di imporre la ricostruzione dell'intero script secondo logica, poichè le porzioni prese in esame volta per volta, saranno esaminate a casaccio;
  • Il terzo è quello di puntualizzare che una qualsiasi procedura, indipendentmente dalla sua complessità, deve essere prima scomposta in moduli e poi ricomposta, tenendo presente che:
  • Un programmatore non conosce un solo linguaggio;
  • Un programmatore conosce più di un linguaggio valutandone pregi e difetti in base a cio che deve sviluppare, scegliendo quello che maggiormente si addice alla tipologia dell'applicativo che sviluppa;
  • Un programmatore ama linguaggi che presentino interfacce di scambio tra tipi differenti con differenti potenzialità: Non è raro, infatti, che porzioni di codice che necessitino di velocità d'esecuzione maggiore vengano scritte in assembly o in C e successivamente integrati nel progetto principale.
Cominciamo dalla fine, ovviamente, e vediamo un esempio di cosa vogliamo ottenere:

Codice: Seleziona tutto

file1 => file2
Copia in corso 8589934590:4294967295 [50%]_
Una volta acquisiti file1 (sorgente) e file2 (destinazione) dalla riga di comando della shell di Windows, la procedura inizia a copiare il file sorgente sulla destinazione, indicando la dimensione del file sorgente:dimensione del file destinazione, nonché la progressione di copia in percentuale.
Sui sistemi a 32 bit, è possibile gestire valori ed indirizzi che non superino il valore pari a 2 elevato alla 32esima potenza meno 1 (4.294.967.295), ma quello che salta subito agli occhi è la dimensione del file sorgente, cioè esattamente il doppio.
Inoltre, per poter fornire 100% come progressione, è necessario che il calcolo della percentuale venga svolto anch'esso a 64 bit. Tale modus operandi non è così strano, basti pensare che la calcolatrice di Windows su un sistema a 32 bit è in grado di eseguire calcoli come se ci si trovasse ad operare su una piattaforma a 64 bit, ma quello che vedremo sarà come integrarne la possibilità nel codice di esempio proposto.
Prima di iniziare, però, è necessario progettare la procedura tenendo presente che la copia è un'operazione di lettura di un'informazione (8 bit) da un dispositivo di memorizzazione ad un altro e che essa avviene sequenzialmente per tutta la lunghezza del file sorgente, fino alla completa riproduzione dello stesso su quello di destinazione.
Le operazioni di lettura, soprattutto quelle di scrittura, tengono impegnato il bus di scambio dati sulla periferica di memorizzazione di massa (notoriamente molto più lente di una RAM ed ancora più lente di una memoria cache) più di ogni altra, pertanto procedere un byte per volta risulterebbe pesante sia per il controller, sia per la CPU, riducendo anche un hw potenziato ricco di processi in esecuzione alla velocità di una lumaca.
La soluzione, sarebbe quella di leggere l'intero contenuto del file sorgente in una prima fase e poi riversare lo stesso contenuto sulla destinazione in una seconda, ma se la dimensione del file sorgente fosse cospicua, attenderne la scrittura per intero forzerebbe la CPU ed il controller a tenere in sospeso i processi in coda più del previsto, con performances generali decisamente scadenti e disattese.
La soluzione maggiormente utilizzata, è quella di riempire un buffer di lettura di un quantitativo non troppo consistente di dati, dando allo scheduler (organizzatore coda dei processi) abbastanza tempo per riuscire a compiere i processi in sospeso, per poi riversare il buffer sulla destinazione fino al completamento dell'operazione di copia.
Forti delle nozioni di base appena esposte, diamo vita alla prima porzione di codice:

Codice: Seleziona tutto

#include <iostream>  
#include <fstream>  
//#include <sys/stat.h>

using namespace std;  
  
int main(int argc, char *argv[]){   

  float perc;
  unsigned int numbuff = 256960;  
  unsigned char buf1[numbuff];
  unsigned int ErrLog = 0; 
 .
 ..
 ...
 return ErrLog;
}
Si evidenziano da subito alcune variabili la cui funzione è chiaramente definita in base al nome indicato, quali numbuff e buf1[], rispettivamente la dimensione del buffer ed il buffer stesso, nonché ErrLog, per la quale è necessario spendere qualche parola in più.
Poichè l'applicativo gira in ambiente console (shell di Windows), esso dovrà essere in grado di fornire un valore pari all'errore riscontrato in uscita dalla funzione main(), così da poterne gestire l'esito attraverso la variabile %ErrorLevel% in una eventuale procedura batch, es:
copia file1 file2
if errorlevel 1 …
if errorlevel 2 …,ecc
Segue la verifica del numero dei parametri immessi da riga di comando e l'eventuale uscita prematura dalla funzione main(), in caso di omissione di uno dei due o di entrambi, tenendo ben presente che la variabile argc considera anche la riga di comando stessa come valore per argv[], il cui indice posizionale è pari a 0 (zero):

Codice: Seleziona tutto

  if(argc!=3) {  
    cout << "Utilizzo: Copy2 <file1> <file2>\n";  
    return 1;  
  } 
Superata questa prima verifica, bisogna fare accesso al percorso del file sorgente ed eventualmente gestire l'errore derivato dall'impossibilità di aprire lo stream in ingresso su quel percorso:

Codice: Seleziona tutto

  ifstream f1(argv[1], ios::in | ios::binary);  
  if(!f1) {  
    cout << "* Impossibile aprire il file sorgente "
         << argv[1] << "\n";  
    return 1;  
  } 
Ad f1 è assegnato un I/O stream in input (ifstream) di tipo binario, cioè in grado di non considerare l'eventuale azione interpretata referente al codice ASCII del carattere stesso; Emblematico è l'esempio del codice ASCII 7 (bell) che, se invocato ed interpretato, emette un beep sonoro sullo speaker del PC.
Il risultato è procedere comunque, qualunque valore abbiano i caratteri di cui si compone il file in lettura.

Codice: Seleziona tutto

  ofstream f2(argv[2], ios::out | ios::binary);  
  if(!f2) {  
    cout << "* Impossibile creare il file di destinazione " 
         << argv[2] << " \n";  
    return 1;  
   }  
Ad f2 è assegnata un I/O stream in output (ofstream) di tipo binario, di tipologia pari alla precedente variabile f1 per forza di cose. Anche qui, va gestito l'errore derivato dall'impossibilità di aprire lo stream in uscita sul percorso di destinazione.
A questo punto ci fermiamo un attimo e ricapitoliamo i punti fissi, costituenti dogma, che un programmatore deve considerare con grande attenzione quando sviluppa un applicativo stendendo il suo codice:
L'utilizzo delle variabili è subordinato alla scelta della loro tipologia, dichiarazione ed inzializzazione;
L'utilizzo del buffer, oltre a considerarlo come variabile soggetta alle regole precedenti, è subordinato anche al suo azzeramento preliminare;
L'esito di una condizione di verifica tra una o più variabili deve prevedere valori certi e sicuri delle stesse, sia alla base della verifica, sia nel prosieguo del flusso d'istruzioni.
Poichè, come si è visto poco sopra, la copia è un processo sequenziale di lettura/scrittura durevole per tutta la lunghezza del file sorgente, è giunto il momento di integrare la gestione dei valori a 64 bit in un ciclo while.

Codice: Seleziona tutto

#include <iostream>  
#include <fstream>  
#include <sys/stat.h>

using namespace std;  
Cio che stiamo cercando, ossia l'header stat.h, si trova nella cartella di contenimento relativa ai files di sistema, pertanto basta includerlo all'interno del sorgente al fine di definire nuove strutture dati, funzioni o subprocedure da richiamare secondo un prototipo predefinito.
Trattasi di una struttura i cui membri possono essere richiamati in lettura/scrittura quando necessario.
I membri, nonché la tipologia di dato a cui essi appartengono, sono definiti sequenzialmente in modo da definire un'area di memoria compartimentata a priori, diversa da una struttura di tipo union, dove i valori dei vari membri appartenenti ad essa possono sovrascrivere sezioni compartimentali differenti dissimili tra loro, posizionate in maniera non necessariamente contigua all'interno della struttura stessa.

Codice: Seleziona tutto

.
..
...
#if defined (__MSVCRT__)
struct _stati64 {
    _dev_t st_dev;
    _ino_t st_ino;
    unsigned short st_mode;
    short st_nlink;
    short st_uid;
    short st_gid;
    _dev_t st_rdev;
    __int64 st_size;
    time_t st_atime;
    time_t st_mtime;
    time_t st_ctime;
};
.
..
...
#if defined (__MSVCRT__)
_CRTIMP int __cdecl  _fstati64(int, struct _stati64 *);
_CRTIMP int __cdecl  _stati64(const char *, struct _stati64 *);
/* These require newer versions of msvcrt.dll (6.10 or higher).  */ 
#if __MSVCRT_VERSION__ >= 0x0601
_CRTIMP int __cdecl _fstat64 (int, struct __stat64*);
_CRTIMP int __cdecl _stat64 (const char*, struct __stat64*);
#endif /* __MSVCRT_VERSION__ >= 0x0601 */
#if !defined ( _WSTAT_DEFINED) /* also declared in wchar.h */
_CRTIMP int __cdecl	_wstat(const wchar_t*, struct _stat*);
_CRTIMP int __cdecl	_wstati64 (const wchar_t*, struct _stati64*);
#if __MSVCRT_VERSION__ >= 0x0601
_CRTIMP int __cdecl _wstat64 (const wchar_t*, struct __stat64*);
#endif /* __MSVCRT_VERSION__ >= 0x0601 */
#define _WSTAT_DEFINED
#endif /* _WSTAT_DEFIND */
#endif /* __MSVCRT__ */
Passiamo, dunque, alla dichiarazione della struttura e successiva definizione ed inizializzazione dei valori dei membri componenti:

Codice: Seleziona tutto

  float perc;
  unsigned int numbuff = 256960;  
  unsigned char buf1[numbuff];
  unsigned int ErrLog = 0; 
  struct _stati64 results, numread, i;
.
..
...
  _stati64 (argv[1], &results); 
  _stati64 (argv[1], &numread);
  _stati64 (argv[1], &i);
  numread.st_size = 0;
Segue l'ultima porzione del codice relativa all'operazione di copia vera e propria con indicazione dell'eventuale errore in lettura dal file sorgente, l'indicazione della progressione in percentuale e quella afferente l'eventuale errore in scrittura del file destinazione; Da notare che l'errore può verificarsi in ogni momento durante la copia (pensate ad un floppy estratto dal lettore o un cd/dvd riscrivibile fallato) con conseguente uscita prematura dal ciclo while e modifica della variabile ErrLog.

Codice: Seleziona tutto

  while(!f1.eof()){ 
      f1.read((char *) buf1, numbuff);  
      if(!f1 && !f1.eof()){
              cout << "\n* Impossibile leggere dal file sorgente "
                   << argv[1] << "\n";  
              ErrLog++;
              break;  
      }
      i.st_size = (f1.eof()?results.st_size-numread.st_size:numbuff); 
      if(f2.write((char *) buf1, i.st_size)){
              numread.st_size += i.st_size;               
              perc = (int)(((float)numread.st_size / (float)results.st_size) * 100);
              cout << "Copia in corso " 
                   << numread.st_size 
                   << ":" 
                   << results.st_size 
                   << " (" 
                   << perc 
                   << "%) ...";
              for(i.st_size=0; i.st_size<80; i.st_size++) cout << "\b";
              memset(buf1, 0, numbuff);
      }else{
            cout << "\n* Impossibile scrivere sul file di destinazione " 
                 << argv[2] << "\n";                 
                 ErrLog++;
                 break; 
      };
  };
Ai lettori più attenti non sarà sfuggita l'istruzione memset utile ad azzerare il buffer di lettura alla fine di ogni cliclo di scrittura, prima che questo venga nuovamente riempito con nuove informazioni dal nuovo ciclo di lettura; Probabilmente, tale accorgimento potrebbe rivelarsi superfluo, sebbene restino validi il primo e secondo dogma sopra riportati, entrambi posti in linea generale a baluardo contro un eventuale buffer overflow riscontrabile ancora oggi in applicativi di maggior complessità, rispetto a quello trattato da questa breve guida.

Codice: Seleziona tutto

  free(buf1);
  f1.close();  
  f2.close();  
  return ErrLog;  
Per niente superfluo, invece, è il trattamento riservato alle variabili f1 ed f2, nonché al buffer buf1 una volta che non siano più utili in uscita dalla funzione main(): Gli streams, se aperti, vanno chiusi e la memoria allocata per il buffer, rilasciata.
Practice feeds Skill,Skill limits Failure,Failure enhances Security,Security needs Practice

Rispondi