Gravar Arquivos, Dúvida Muito Básica em C++ [RESOLVIDO]

1. Gravar Arquivos, Dúvida Muito Básica em C++ [RESOLVIDO]

Nick Us
Nick-us

(usa Slackware)

Enviado em 16/05/2020 - 21:01h

Tentando entender o básico do básico sobre gravar arquivos em C++, Montei um exemplo baseado no que li na Internet, reduzindo ele ao máximo que pude, para compreender cada linha separadamente, foi então que surgiu a dúvida.

No exemplo que montei percebi que preciso apenas dessa linha abaixo para criar e gravar um arquivo:
std::ofstream W_MyFile("Test.txt"); 

Acontece que no site http://www.cplusplus.com/doc/tutorial/files/ eles inistem em usar myfile.open assim como em todos exemplos da Internet que vi.

Então analisem o seguinte caso:
1) O Comando ofstream ele cria o arquivo pra mim, então não tenho que testar nada e nem preciso, porque tenho certeza 100% que o arquivo não existe!
2) Para que então eu usaria o comando OPEN? Se eu já abri o arquivo para gravação com o comando ofstream?

Então eu gostaria de saber se estou fazendo alguma estupidez em usar apenas offstream para gravar meu arquivo?
Pra facilitar o entendimento EU quero usar assim o mais simplificado possível, isso que estou fazendo abaixo é errado?
std::ofstream W_MyFile("Test.txt");
W_MyFile << "Good Morning\n";
W_MyFile.close();

Usei 3 comandos para criar meu arquivo, compilei com o máximo de Warnings! E tudo passou e funcionou!



  


2. MELHOR RESPOSTA

Paulo
paulo1205

(usa Ubuntu)

Enviado em 17/05/2020 - 04:37h

Caro Nick,

Eu imagino que você tenha seus motivos de querer acelerar a entrega de algum código que você queira fazer. Mas se o seu objetivo for o de aprender direito o material com o qual você está lidando, seria interessante você parar para estudar de modo mais sistemático. Codificar por cópia e colagem pode até produzir resultados que funcionem, mas eu diria — por experiência própria — que, como método de trabalho, é muito ineficiente e tende a ser frustrante.

Considere adquirir um bom livro texto. Eu indiquei um recentemente, mas que é meio caro (e com o dólar acima dos R$6,00, deve estar ficando ainda mais caro). Não tenho outras indicações a fazer porque não tenho mais usado livros introdutórios, mas pode ser que alguém mais as tenha, e você mesmo pode fazer uma pesquisa na Internet a esse respeito.


Se você pegar um livro bom sobre C++, uma das coisas que devem aparecer relativamente cedo na leitura é a técnica conhecida como RAII (de “Resource Acquisition Is Initialization”, ou “Aquisição de Recursos É Inicialização”).

Como você deve saber, cada classe do C++ possui um ou mais construtores e um destrutor. Quando um objeto de uma classe é criado, um dos construtores é invocado para colocar aquele objeto num estado que seja pronto para uso, o que frequentemente envolve reservar algum recurso (tal como memória, acesso a arquivo, abertura de canal de comunicação com a rede ou com outro processo etc.). Ao final da vida do objeto, o destrutor é invocado, como meio de devolver, de modo ordenado, recursos que tenham sido usados pelo objeto desde o momento de sua criação ou em algum outro momento do seu tempo de vida.

A ideia da técnica RAII é se valer do fato de que todo objeto começa com um construtor e termina com o destrutor para fazer com que todo recurso que precisa ser adquirido e depois liberado seja controlado por um objeto, de modo que a aquisição do recurso ocorra no momento de sua construção e a liberação necessariamente aconteça quando ele for destruído (ou desconstruído, que talvez seja uma expressão melhor para passar a ideia de que é um processo ordenado).

Mas por que isso?

Principalmente para lidar de forma adequada casos de erro. Até porque, entre outras coisas, oferece uma sintaxe mais simples.

Quando você faz gestão de recursos no estilo do C (que também é possível em C++, mas normalmente é evitada), geralmente a aquisição do recurso e sua liberação estão em partes completamente distintas do código, e isso gera um risco significativamente grande de que ocorram erros na elaboração dos fluxos de execução que podem levar a situações tais como esquecer de liberar o recurso ou tentar usá-lo depois de já ter sido liberado. Esse problema fica ainda mais grave se o código tiver de manipular vários recursos ao mesmo tempo (a manipulação simultânea de múltiplos recursos é, inclusive, a causa mais comum para justificar o uso de goto em C, ou então os programas escritos em diagonal, que são piores ainda de ler).

Em C++, usando RAII, a forma de fazer é mais simples porque o escopo que contém o objeto controla seu tempo de vida e, por conseguinte, do recurso que ele controla. Se ocorrer algum erro com o objeto, ele vai disparar uma exceção. Se a exceção ocorrer durante a construção do objeto e tal objeto tiver vários componentes internos (os quais provavelmente também usam RAII), aqueles que já tiverem sido inicializados serão destruídos, e então a exceção será propagada de volta para quem tentou construí-lo. Se a exceção ocorrer após a construção, então a exceção será propagada para quem invocou a função que a provocou. Quando a exceção é recebida pelo chamador o fluxo de execução vai sair do escopo em que estava, de modo que o tempo de vida de todos os objetos que já tiverem sido construídos terá chegado ao fim, o que implica que seus destrutores serão chamados.

Uma comparação de código provavelmente ilustrará isso melhor. Os trechos abaixo mostram exemplos de uma função que depende de três recursos diferentes obtidos dinamicamente, para que você possa comparar os jeitos típicos de fazer em C e em C++ (incluindo exemplos de erros de implementação, para você ter uma ideia dos problemas). Em todos os casos, a função f() retorna um valor verdadeiro em caso de sucesso ou falso em caso de falha ou erro. Se você descontar os comentários explicativos do meio dos códigos, verá que o código em C++ é mais simples, mais expressivo e mais fácil de manter corretamente.

// Exemplo 1, em C usando goto.
int f(const char *param1, const char *param2, const char *param3){
int ret_val=0; // Pressupõe falha.

T_REC1 *p_r1=aloca_rec1(param1);
if(!p_r1)
goto libera_p_r1;

T_REC2 *p_r2=aloca_rec2(param2);
if(!p_r2)
goto libera_p_r2;

T_REC3 *p_r3;
if(!p_r3)
goto libera_p_r3;

// Aqui, p_r1, p_r2 e p_r3 apontam para recursos válidos. Começa a computação que os usa.

// ...
// ...

// XXX: Neste ponto, digamos que ocorreu uma operação ilegal envolvendo um dos recursos. O programador
// XXX: que fez manutenção do código sabe que f() retorna 0 em caso de falha, e simplesmente fez isto aqui:
if(!combina(p_r1, p_r2))
return 0;
// XXX: Viu? Ele saiu da função sem liberar nenhum dos três recursos!

// Se a função de combinação acima tiver funcionado, o código continua abaixo. Suponha que não há outros erros.

// ...
// ...

// Chegou ao final da computação. Ajusta ret_val para indicar sucesso.
ret_val=1;

libera_p_r3:
libera_rec3(p_r3);

libera_p_r2:
libera_rec2(p_r2);

libera_p_r1:
libera_rec1(p_r1);

return ret_val;
}

// Exemplo 2, em C usando código diagonal (NÃO USE!).
int f(const char *param1, const char *param2, const char *param3){
int ret_val=0; // Pressupõe falha.

T_REC1 *p_r1=aloca_rec1(param1);
if(p_r1){
T_REC2 *p_r2=aloca_rec2(param2);
if(p_r2){
T_REC3 *p_r3;
if(p_r3){
// Aqui, p_r1, p_r2 e p_r3 apontam para recursos válidos. Começa a computação que os usa.

// ...
// ...

// XXX: Neste ponto, digamos que ocorreu uma operação ilegal envolvendo um dos recursos. O programador
// XXX: que fez manutenção do código sabe que f() retorna 0 em caso de falha, e simplesmente fez isto aqui:
if(!combina(p_r1, p_r2))
return 0;
// XXX: Viu? Ele saiu da função sem liberar nenhum dos três recursos!

// Se a função de combinação acima tiver funcionado, o código continua abaixo.
// Suponha que não há outros erros.

// ...
// ...

// Chegou ao final da computação. Ajusta ret_val para indicar sucesso.
ret_val=1;

libera_rec3(p_r3);
}
libera_rec2(p_r2);
}
libera_rec1(p_r1);
}

return ret_val;
}

// Exemplo 3, em C++ usando RAII, interceptando exceções e deixando que a função apenas sinalize
// sucesso ou erro através do valor de retorno. Poderia ser ainda mais simples se, em lugar de tratar
// exceções aqui eu as deixasse vazar para o chamador da função, mas aí o programa não seria equi-
// valente à versão em C.

// f() utiliza tipo de retorno bool, em vez de int, e parâmetros que são referências a std::string. que, por
// si só já é uma maneira de implementar strings que usa RAII.
bool f(const std::string ¶m1, const std::string ¶m2, const std::string ¶m3)
try {
// Como a função inteira depende de RAII, eu usei o tratamento de exceções no lugar do bloco da
// função. Se eu precisasse de algo mais específico, poderia ter usado um bloco de função comum,
// movendo o trecho sujeito a exceções para dentro dele.

T_REC1 r1(param1); // Note que não são ponteiros.
T_REC2 r2(param2);
T_REC3 r3(param3);
// Se ocorrer exceção durante a construção de algum dos objetos, apenas os que já tiverem
// sido construídos terão seus destrutores invocados.

// Aqui, r1, r2 e r3 contêm recursos válidos (não meramente apontam). Começa a computação que os usa.

// ...
// ...

// Neste ponto, digamos que ocorreu uma operação ilegal envolvendo um ou mais dos recursos em uma função
// que não dispara exceções. O programador que fez manutenção do código sabe que f() retorna false em caso
// de falha, e simplesmente fez isto aqui:
if(!combina(r1, r2))
return false;
// Não tem o menor problema! O return faz com que o fluxo sai do escopo, o que implica chamar os
// destrutores do objeto normalmente.

// Se a função de combinação acima tiver funcionado, o código continua abaixo. Suponha que não há outros erros
// o que todos os erros que houver sejam reportados por meio de exceções.

// ...
// ...

// Chegou ao final da computação. Simplesmente retorna true.
return true;

// Note que não preciso desalocar nada. O fim do escopo de existência dos objetos provoca a chamada de
// seus destrutores automaticamente.
}
// O tratamento de exceções está muito simples. Poderia ser mais complexo, dependendo da classe de erro.
catch(...){
return false;
}



Classes como std::ifstream e std::ofstream são classes para entrada e saída em arquivos que implementam a técnica de RAII. Compare as duas maneiras de gravar meu nome num arquivo: em C com <stdio.h> e em C++ com <fstream> (sem tratamento de erros).

#include <stdio.h>

int main(void){
FILE *fp=fopen("/tmp/nome.txt", "w"); // Lembrando que o modo "w" provoca truncamento do arquivo.
fputs("Paulo\n", fp);
fclose(fp);
}

#include <fstream>

int main(){
std::ofstream("/tmp/nome.txt", std::ios::trunc) << "Paulo\n";
}


Na versão em C++ eu nem precisei de uma variável para associar o objeto que representa o arquivo. Graças ao RAII, um objeto temporário é construído com um construtor que recebe o nome do arquivo e a instrução de criar/truncar o arquivo, é usado como operando esquerdo de << e, ao final da expressão, quando o temporário sai do seu contexto de existência, é automaticamente desconstruído, o que implica fechar o arquivo.


Quanto a razões para preferir std::ofstream::open() sobre a abertura feita automaticamente pelo construtor e std::ofstream::close() sobre o fechamento feito implicitamente pelo destrutor, há algumas razões que me ocorrem agora. Entre outros possíveis motivos (que não me ocorrem agora), eis alguns:

  • Para permitir o emprego de exceções para tratamento de erros. Apesar de ter a característica de RAII, as classes de streams de arquivos não costumam usar exceções para sinalizar erros, até porque uma eventual “falha” em determinadas operações não caracteriza uma situação realmente excepcional (por exemplo: quando você chega ao final de um arquivo aberto para leitura, ou quanto você pede para abrir um arquivo sobre o qual não tem permissão de acesso). Mesmo assim, você pode pedir que o objeto use exceções para comunicar algumas situações de falha, mas só depois que o objeto já estiver construído. Assim sendo, se você quiser que a abertura do arquivo sinalize erro por meio de uma exceção, tem de criar o objeto sem associá-lo a arquivo nenhum, chamar a função que habilita o envio de exceções sobre esse objeto, e então abrir o arquivo explicitamente.

  • Para poder tratar erros na hora de fechar o arquivo. É possível que determinadas operações, sobretudo em arquivos abertos para escrita, só sinalizem erro na hora de fechar o arquivo (por exemplo: você mandou escrever dados no arquivo, mas o volume não foi suficiente para estourar o cache do sistema operacional, de modo que o SO só tentou gravar efetivamente em disco quando você mandou fechar o arquivo, mas nessa hora faltou espaço livre ou deu algum erro de disco). O fechamento automático por meio do destrutor não vai reportar esse erro (mesmo que você esteja com exceções habilitadas, uma vez que os destrutores do C++ não podem deixar vazar exceções). Se você quiser garantia de integridade dos dados após o fechamento, tem de chamar std::stream::close() explicitamente.

  • Porque nem sempre você tem o nome do arquivo na hora em que o objeto é criado. Um exemplo que me vem à mente é quando você tem um array ou um container da biblioteca padrão cujos elementos sejam streams ou tipos compostos contendo streams, que são preenchidos (abertos) ao longo da execução do programa. Nesses casos, os objetos correspondentes a cada elemento são criados, no momento da alocação do array ou container, com o construtor default, que não associa nenhum arquivo a esse objeto, que são posteriormente associados a arquivos por meio de chamadas a função-membro open().


Qual forma você deve preferir no seu programa vai depender da característica do problema que você estiver tratando. Se você não pretende, por exemplo, tratar erros na hora de fechar o objeto do tipo std::ofstream, não precisa chamar explicitamente std::ofstream::close() sobre ele, mas pode perfeitamente deixar que o destrutor cuide de liberá-lo. Minha sugestão e que você prefira sempre a forma mais simples, desde que a tal forma seja suficiente para atender suas necessidades.


... Então Jesus afirmou de novo: “(...) eu vim para que tenham vida, e a tenham plenamente.” (João 10:7-10)

3. Re: Gravar Arquivos, Dúvida Muito Básica em C++ [RESOLVIDO]

José
DeuRuimDotCom

(usa Linux Mint)

Enviado em 18/05/2020 - 01:44h

Para uma bibliografia inicial em C, recomendaria DAMAS ou BACKES, para ficar nos brazukas.

Paulo, o senhor é um dos grandes patrimônios desse site, suas lições aqui são, em minha humilde opinião, a melhor fonte na internet brasileira da linguagem C, dadas a profundidade e a clareza de suas análises. Por suposto, sugestões bibliográficas suas são muito bem-vindas, inclusive de nível intermediário e avançado, mas, se me permite repetir o que já disse alhures, deveria escrever seu próprio livro; não tenho a menor dúvida de que seria um guia definitivo do velho e bom C.


4. Re: Gravar Arquivos, Dúvida Muito Básica em C++

Nick Us
Nick-us

(usa Slackware)

Enviado em 23/05/2020 - 19:29h

paulo1205 escreveu:Caro Nick,
Eu imagino que você tenha seus motivos de querer acelerar a entrega de algum código que você queira fazer. Mas se o seu objetivo for o de aprender direito o material com o qual você está lidando, seria interessante você parar para estudar de modo mais sistemático. Codificar por cópia e colagem pode até produzir resultados que funcionem, mas eu diria — por experiência própria — que, como método de trabalho, é muito ineficiente e tende a ser frustrante.

Você tem razão, mas não estou programando profissionalmente e nem pra ninguém. Eu estou montando programas pra mim mesmo, sendo que os únicos úteis são 2 Databases um de Contatos e outro de Dicas de C e C++ para eu consultar. E claro com as perguntas monto pequenos programas exemplos para eu consultar qdo for usar. Esses 2 databases que quero ver funcionando, eu já fiz e refiz eles inúmeras vezes, e não estão prontos, mas não é um problema, pq são essas modificações que estão me fazendo aprender.

Eu estudo por alguns livros, mas eu compreendo melhor qdo estou vendo elas acontecerem! São os exemplos! Eu demorei um tempo pra entender o debug e seus warnings, comecei compilando sem nenhum parametro. E foi essa experiência que me levou a compilar hoje com (-O3 -Wall -pedantic -pedantic-errors -Werror) todos esses parametros e aprender a fazer funcionar! E pra isso foram inúmeros posts que vc explicou tanto pra mim, quanto para muitas outras pessoas sobre o Debug que finalmente entendo ele melhor.

Os problemas que enfrento que ao ler, sem exemplos minha mente não grava! Então eu pego 1 pedaço apenas, uma palavra, um comando, e procuro entender ele o melhor que posso, coloco ele em prática, testo e tento modificar, e procuro entender sua Syntax para saber como aquilo funciona!

Se você pegar um livro bom sobre C++, uma das coisas que devem aparecer relativamente cedo na leitura é a técnica conhecida como RAII (de “Resource Acquisition Is Initialization”, ou “Aquisição de Recursos É Inicialização”).

Começei a ler sobre C++ a pouco, pois eu estava muito mais focado em C anteriormente, até ainda estou, mas percebi que terei que usar C++ em muitos casos. E lendo sobre o mesmo, existem muitas coisas diferentes, e que agora estou tentando usar de forma diferente, como abrir/fechar arquivos que é bem diferente do C.
Classes como std::ifstream e std::ofstream são classes para entrada e saída em arquivos que implementam a técnica de RAII. Compare as duas maneiras de gravar meu nome num arquivo: em C com <stdio.h> e em C++ com <fstream> (sem tratamento de erros).

#include <stdio.h>

int main(void){
FILE *fp=fopen("/tmp/nome.txt", "w"); // Lembrando que o modo "w" provoca truncamento do arquivo.
fputs("Paulo\n", fp);
fclose(fp);
}

#include <fstream>

int main(){
std::ofstream("/tmp/nome.txt", std::ios::trunc) << "Paulo\n";
}


Na versão em C++ eu nem precisei de uma variável para associar o objeto que representa o arquivo. Graças ao RAII, um objeto temporário é construído com um construtor que recebe o nome do arquivo e a instrução de criar/truncar o arquivo, é usado como operando esquerdo de << e, ao final da expressão, quando o temporário sai do seu contexto de existência, é automaticamente desconstruído, o que implica fechar o arquivo.

Esse exemplo que vc deixou com as explicações, foram ótimos, pois deu um caminho para entender, e que ainda estou lendo no site cplusplus.
Pois ainda estou montando exemplos para ver na prática, e um dos primeiros problemas que tive foi sobre a variável pq no meu exemplo usei variável e não sabia re-usá-la e aqui vc nem usou uma variável, achei super interessante e estou estudando o funcionamento desses comandos.
Mas preciso montar os exemplos para entender se sei fazer, ofstream, ifstream, fstream estou lendo sobre isso e ainda criando exemplos funcionais. Quando uso um exemplo pronto como o que vc deixou aqui, gosto de estudar ele, como funciona, pois assim poderei modificar, adaptar. Inicialmente aprendi o que é std:: agora eu sei onde colocar, o porque ele está no código, e inclusive qdo não colocam ele. Ou seja, Compreendi bem o que ele é e faz. Então posso fazer do meu jeito! Escolho um jeito, e depois analiso se a escolha que fiz é correta ou não, como uma vez perguntei sobre o fwrite... Então estudo mais qdo dizem que minha escolha foi errada! Pq obviamente alguma coisa passou e eu não entendi.

Teve um livro que vc indicou eu achei ele em Inglês na Internet, essas indicações SEMPRE ajudam e muito!
E ainda estou estudando essa resposta, não terminei ainda!

Costumo usar as respostas para criar os exemplos que funcionam para que eu não esqueça.
Eu até criei exemplo de abrir, fechar, gravar, testar arquivos em C++, porém preciso conhecer mais a fundo esses comandos, que estou estudando e tentando entender.





5. Re: Gravar Arquivos, Dúvida Muito Básica em C++ [RESOLVIDO]

Nick Us
Nick-us

(usa Slackware)

Enviado em 23/05/2020 - 19:36h

DeuRuimDotCom escreveu:
Para uma bibliografia inicial em C, recomendaria DAMAS ou BACKES, para ficar nos brazukas.

Mesmo minha pergunta sendo C++, fiquei interessando no que vc falou para C, porque gosto do C e ainda estou aprendendo C. Mas não entendi o que vc quis dizer com DAMAS ou BACKES. Se puder dar um link ajudará muito. Eu gosto de comparar livros em C, mesmo na maioria das vezes o conteúdo sendo repetitivo e igual, sempre tem algo diferente para aproveitar, até mesmo a forma de explicar, o que muda uma perspectiva de aprendizado.

Digo isso, pq já modifiquei inúmeras vezes a forma de declarar algo, pq vi exemplos diferentes e declarações diferentes, e até mesmo idéias diferentes sobre o mesmo assunto.








Patrocínio

Site hospedado pelo provedor RedeHost.
Linux banner

Destaques

Artigos

Dicas

Tópicos

Top 10 do mês

Scripts