Programar em C++/Imprimir



O livro Programar em C++ tem por objetivo apresentar os fundamentos desta linguagem, de modo que o estudante possa desenvolver diferentes tipos de softwares em alto e baixo nível para os diversos ambientes existentes, desde aplicações para GNU/Linux ou Windows até programas para microcontroladores, além de fornecer a base para os estudos avançados de C++.

Por ser um livro específico sobre a linguagem C++, é altamente recomendável que o leitor tenha conhecimentos prévios sobre a linguagem C.

Espera-se que este livro aborde:

  • Aspectos teóricos
  • Aspectos práticos
  • Os erros comuns

Para tanto cada tópico deverá ter uma explicação teórica, citar os erros mais comuns e exercícios.

Imagine que você deve fazer um programa para fazer a máquina de um pequeno relógio de pulso funcionar, então você pensa:

  • Bom, isso pode ser feito com Assembly...
  • Porém, pensando melhor, você decide mudar de linguagem quando você pondera.
  • O problema maior é que se eu tiver que mudar o processador do relógio, vou ter que refazer o programa. É melhor usar linguagem "C".
  • Depois você termina por avaliar outra possibilidade:
  • Bem, se eu já estou pensando em "C" é melhor usar "C++", depois vai ser mais fácil de entender o código, reaproveitar em outras coisas e ainda vai ser mais fácil de expandir para outros modelos de relógio.

E assim é o que podemos perceber como C++ é poderosa, flexível e abrangente. Ela pode ser usada para programar qualquer tipo de hardware, desde os mais simples até os mais complexos. Além disso, C++ é uma linguagem que gera programas em código de máquina, que funcionam com ou sem a participação de sistemas operacionais no dispositivo.

Alguns profissionais afirmam que C++ é a linguagem mais poderosa que existe, veja algumas características dela:

  • É um superconjunto da linguagem C, e contém vários melhoramentos;
  • Deu origem a grandes linguagens como Java e D;
  • É a porta para a programação orientada a objetos;
  • C++ pode virtualmente ser efetivamente aplicado a qualquer tarefa de programação;
  • Há vários compiladores para diversas plataformas tornando a linguagem uma opção para programas multiplataforma.

A linguagem C++ é utilizada em projetos como:

Até ao momento foram realizadas 3 grandes revisões à linguagem:

  • 1ª em 1985;
  • 2ª em 1990;
  • 3ª em 1998 a que deu origem ao ANSI \ ISO standard a que ficou comummente denominada de Standard C++. Esta versão é suportada por todos os compiladores C++ famosos incluindo Microsoft’s Visual C++, Borland’s C++ Builder e GCC. Esta foi revista em 2003.

C++ é considerada uma linguagem que está entre linguagem de alto nível (em inglês, high level language) e linguagem de baixo nível (em inglês, low level language). Dito de outra forma, é uma linguagem que está próxima da linguagem humana (linguagem de alto nível), mas ao mesmo tempo permite estar próximo da maneira como o computador processa, próximo do Assembly (uma linguagem de baixo nível).

Quem sabe programar em C++, é capaz de programar C, devido à semelhança entre as linguagens e o fato do C++ ser uma extensão do C. Contudo o C não é completamente um subconjunto do C++. Grande parte de código C pode ser perfeitamente compilado em C++, mas existem algumas pequenas diferenças sintáticas e semânticas entre as linguagens que tornam alguns trechos de código C válidos em código C++ inválido, ou códigos que exibem comportamentos diferentes em cada linguagem.


Algumas diferenças básicas:

  • O C permite a conversão implícita entre o tipo de dado void* para ponteiros para outros tipos, algo que o C++ não permite.
  • O C permite que constantes de caracteres sejam inseridas em chamadas de funções com parâmetros tipo char*, em C++ é preciso declarar o parâmetro como const char *;

Além destas pequenas diferenças, C++ tem um conjunto de características que a torna fundamentalmente diferente de "C". Esse conjunto, torna possível programar em C++ de um modo totalmente diferente do modo de programar da linguagem "C". O que traz a diferença é o modo da orientação na montagem do código.

Chamamos o modo de programar em "C" de orientado a procedimentos e chamamos o modo do "C++" de orientado a objetos. Muitas pessoas confundem as coisas quando começam a programar usando um compilador C++, pois esta linguagem permite programar nos dois modos. Essa é uma das características que a torna mais flexível.

Apesar de C++ permitir programar em modo orientado a procedimentos, podemos dizer que nestes casos estamos programando em "C", usando um compilador C++. Quando usamos C++ programamos em modo orientado a objetos. Devido a estas características, o C++ permite programar em modo misto, ou seja, escrevendo partes do código orientadas a procedimentos e outras orientadas a objetos.

As diferenças entre os dois modos de programar serão esclarecidas nos capítulos subsequentes. Por hora nos basta deixar claro que os dois modos são diferentes. Usar estes dois modos de programar ao mesmo tempo é uma das facilidades que o C++ permite, enquanto que outras linguagens orientadas a objetos como Java, Eifel, etc, não permitem.


 

Esta página é um esboço de informática. Ampliando-a você ajudará a melhorar o Wikilivros.

Pensando no código

editar

Considerando o conjunto de operações e eventos que nosso programa deve executar temos diversas maneiras de criar o código, porém o mais difícil é criar um código eficiente, rápido e compacto. Na verdade, diversos fatores podem interferir nestes aspectos da programação, entre eles, a escolha do compilador, o método de estruturar o código, a orientação do mesmo, etc... Em termos gerais, um código torna-se mais próximo do ideal a medida que suas partes tornam-se mais simples de se analisar e quando todos os processos estão bem definidos e especializados. Quando temos um código que contém muito mais exceções do que regras, este precisa de uma reestruturação.

Podemos definir C++ como um "superconjunto" da linguagem C, ou seja, uma linguagem com mais funcionalidades que a linguagem C. Embora este seja o ponto de vista de quem já tem um conhecimento da linguagem C, ela é muito mais que isto. Podemos mudar completamente a forma de criar o programa quando usamos os recursos avançados da linguagem, as estruturas de decisão (por exemplo, if-else ou switch) podem ser simplificadas e a organização do código pode ser bem mais globalizada e genérica, possibilitando a reutilização do código em diversas situações diferentes.

Vejamos como as funcionalidades da linguagem C++ podem ajudar a redefinir os meios de programação que aprendemos com o bom e velho estilo C.

Dois modos de programar

editar

Observando o modo de programar que as linguagens oferecem desde os primórdios da computação, podemos notar vários métodos que foram sendo superados e outros que se estabelecem por um momento. O modo de programar mais usual e bem mais difundido é conhecido como modelo estruturado sequencial. Em síntese, refere-se a forma de programar onde uma instrução segue a outra numa sequência que inicia e termina em um fluxo parcialmente "previsível".

A programação estruturada ainda pode ser classificada em dois modos: um orientado a procedimentos e outro orientado a objetos. Os dois podem ser confundidos por quem não tem muita experiência, mas o uso de um ou do outro implica em características próprias para cada caso. Logo, é preciso entender bem os conceitos antes de definir se um código é procedural ou orientado a objetos.

O modelo sequenciado procedural é bem simples de implementar, porém, aumenta a complexidade para tarefas mais bem trabalhadas e sofisticadas. Isto ocorre devido a estrutura do modelo, que exige rotinas cada vez mais extensas. Não raramente é possível encontrar rotinas que, em alguns casos, tornam-se comparáveis a programas completos, usando como referência o número de linhas de instruções. O uso deste modelo, muitas vezes dificulta a manutenção e expansão do código, caso isto não seja feito de maneira muito bem organizada.

O segundo modelo é o orientado a objetos. O que significa isso e o que muda no modo de programar, caso o adotemos em vez do modelo anterior?

A orientação define o modo de abordar o problema para tentar solucioná-lo:

  • O modelo de orientação a procedimentos se preocupa em fornecer meios para resolver o problema sem contabilizar, a princípio, os dados que serão usados durante o processo.
  • O modelo de orientação a objetos se preocupa com os elementos que são necessários para a solução de um problema. Sob este ângulo, os dados são os elementos principais na análise do problema.

Este livro traz uma visão dos problemas sob a óptica da orientação a objetos, enquanto que o livro "Programar em C" traz a análise sob a óptica da orientação a procedimentos. Isto não quer dizer que devamos escolher a orientação a objetos como o modo mandatório de programação, mas que podemos contar com seus recursos sempre que esta escolha facilite a resolução do problema. Portanto, cabe sempre uma análise para definir se o problema é melhor tratado por uma abordagem orientada a objetos ou a procedimentos.

Um pouco sobre orientação a objetos

editar
 
Wikipedia
A Wikipédia tem mais sobre este assunto:
Orientação a objetos

A programação orientada a objetos é um paradigma de programação que visa organização, produtividade e sustentabilidade.

A apresentação dos conceitos de orientação a objetos é bastante abrangente, o que implica na abordagem de diversos aspectos, como modelagem, estudo de performance de modelos, aplicabilidade de técnicas, estruturação de objetos, otimização, manutenção do código, entre outros. Por este motivo, nosso objetivo aqui não é apresentar a orientação a objetos em sua totalidade. Para um estudo mais detalhado do tema sugerimos o livro POO, que trata especificamente deste tema. O objetivo aqui é apresentar como a orientação a objetos se aplica na linguagem C++, porém os conceitos aqui apresentados devem ser suficientes para a estruturação de programas de bom nível.

A ideia principal por trás do modelo de programação orientado a objetos está em transformar entidades do mundo real em identificadores dentro do programa (objetos), trabalhando-os como entidades da linguagem que possuem características e operações próprias. Esta abordagem transforma o programa em um meio de simulação de situações virtuais por meio de entidades de código que têm comportamento predefinido. Esta abstração é uma aliada do programador por permitir idealizar sistemas mais sofisticados de uma maneira bastante intuitiva.

Todas as linguagens orientadas a objetos contêm os princípios de:

  • Encapsulamento
É um mecanismo para esconder os detalhes envolvidos no processamento de uma ação. Por exemplo, quando usamos um telefone, não precisamos lidar diretamente com o circuito interno; a interface do telefone cuida desse problema.
  • Polimorfismo
Isso permite o uso de uma única interface ― uma única forma de uso ― para objetos de tipos diferentes; em particular, a mesma interface para objetos de uma classe e objetos de classes derivadas dessa.
  • Herança
Como o nome sugere, isso permite que uma classe herde de outra suas características, podendo também introduzir suas próprias ou alterar as características herdadas. O uso de herança acaba poupando trabalho e melhorando a organização do código.

Paradigmas da Programação:

editar

Desde o início da programação, já passamos pelos seguintes paradigmas:

  • Não estruturada - exemplos: COBOL, FORTRAN, BASIC (anos 50-60)
  • Procedimental ou Procedural - exemplos: C, Pascal (anos 70)
  • Modular - exemplo: Modula II (anos 80)
  • Abstração de tipos de dados - exemplo: Ada (anos 80)
  • Programação Orientada a Objetos - exemplos: C++, Java, Delphi (Object Pascal) entre outras. (décadas 80-90-2000)

Objetos

editar

Objeto é, genericamente, uma entidade de armazenamento e manipulação de dados. O mesmo deve ser criado para processar os dados que armazena e recebe, sendo sensível a entradas do programa principal para fornecer as saídas esperadas pelo mesmo. Por estes motivos o objeto deve ser pensado como uma entidade de dados autônoma, encarregada de processar todos os dados que mantém.

Da mesma forma que podemos usar tipos de dados nativos da linguagem podemos criar nossos tipos de dados. Na linguagem C podemos criar tipos de dados compostos que chamamos de estruturas, estes são criados com a palavra chave struct. C++ possibilita o uso de estruturas de dados e introduz um novo tipo chamado de classe. Como o nome sugere, uma classe refere-se a um conjunto de características dadas a um grupo de "indivíduos", ou seja, grupo de objetos. Por este motivo, classe é a definição de tipo de objeto.

Em C++ as classes de objetos são criadas através da palavra chave class. Esta nomenclatura é usada por muitas outras linguagens de programação mais caracteristicamente restritas a orientação a objetos. Estes aspectos facilitam um pouco o aprendizado por programadores já familiarizados com estas linguagens quando iniciam a programação em C++.

O processo de criação de um objeto segue a sequência:

  • Definir os dados e procedimentos que a classe deve conter;
  • Criar a classe de objetos;
  • Declarar (instanciar) o objeto.

A definição de uma classe de objetos deve ser feita de forma a tornar, preferencialmente, todos os dados protegidos de interferências de códigos externos ao objeto. Por este motivo um objeto deve ser uma parte do código do programa com uma certa autonomia. Este deve ter controle sobre seus dados e ser capaz de provê-los e lidar com eventos a eles relacionados. Dentro de seu escopo de responsabilidades, a entidade deve essencialmente "ter vida própria".

Olá mundo!

editar

É comum, no aprendizado de uma linguagem de programação, que seu primeiro programa faça com que o computador exiba "Olá mundo!". Na linguagem C++ este primeiro programa já introduz muitos conceitos sobre a linguagem. Veja o código do nosso primeiro programa:

#include <iostream>

using namespace std;

int main ()
{
  cout << "Olá mundo!"; 
  return 0;
}

Assim como para começar a dirigir não é necessário saber toda a mecânica do carro, para programar não precisamos logo de início nos prender a todos os detalhes.

No programa acima, vamos dar atenção apenas ao conteúdo que se encontra entre as chaves:

 {
   cout << "Olá mundo!";
   return 0;
 }
  • cout << "Olá mundo!";

A palavra cout vem de Console OUT (saída do console), onde geralmente temos a saída no monitor. O cout é seguido do operador << e da frase que se quer informar entre aspas: "Olá mundo!", intuitivamente, isso nos leva a ideia de que a sequência de caracteres será levada ao cout.

  • return 0;

Este comando termina o programa, o estudaremos melhor no capítulo sobre funções e retornos.


ATENÇÃO:

Caso seu sistema operacional seja o Microsoft Windows, você deve adicionar imediatamente antes de return 0; a seguinte linha:

  • system ("pause");

A função system() executa um comando do Windows. É como se o próprio usuário digitasse pause no prompt do MSDOS. Este comando deve ser evitado, pois diminui a portabilidade do programa, já que pause só existe nos sistemas Microsoft. No entanto, se está usando Windows é necessário adicionar esta linha, caso contrário o computador escreveria "Olá mundo!" e fecharia o programa antes que pudéssemos ler qualquer coisa. Uma forma elegante de lidar com estas peculiaridades do Windows é usar predefinições, de forma que o programa seja portável para qualquer sistema operacional:

#if defined(_MSC_VER)
// estas linhas serão executadas apenas quando o programa
// for compilado por alguma versão do Microsoft Visual C
  system("pause"); 
#endif

Em sistemas parecidos com UNIX, como GNU/Linux ou FreeBSD, pode-se usar um terminal de linha de comando facilmente, pois os mesmos possuem o recurso facilmente acessível, mesmo quando o usuário está usando a interface gráfica. Por isso, para esses sistemas um comando para solicitar pausa ao sistema não é necessário.

Entrada de dados e comentários no código

editar

Comentário é um recurso muito útil em programação. Ele permite que o programador adicione texto ao programa para facilitar o entendimento do programa por parte de outros programadores, ou até dele mesmo. Os comentários são usados para explicar trechos de código, adicionar cláusulas e qualquer texto em geral.

Vamos agora para um programa mais completo com entrada de dados e comentários dentro do código:

// Este é um comentário de uma linha

/*
  Este é 
  um comentário
  de várias
  linhas 
*/

#include <iostream>

using namespace std;

int main ()
{
  int x;
  cout << "Digite um número: ";
  cin >> x;
  cout << "\nVocê digitou o número: " << x << endl;
  return 0;
}

Comentários no programa

editar

Observemos esta linha:

 // Este é um comentário de uma linha

Esta é uma linha de comando para o preprocessador (ou precompilador).

O que é o preprocessador? Durante o processo de montagem do programa em formato binário existem três fases principais: O preprocessamento, a compilação e a fase de ligação (link). O preprocessador é um programa invocado pelo compilador para remover comentários e substituir certas partes do programa conforme a necessidade do código criado pelo programador.

O preprocessador faz algumas alterações no texto fonte, que basicamente consistem em eliminar pedaços de código e/ou substituí-los por outros (copy-paste). Enfim, o mesmo altera o código fonte contendo comandos iniciados com # e outros comandos específicos, por outro código sem comandos de preprocessamento, puramente em linguagem C++.

Ao analisar o código, o preprocessador encontra a sequência // e vai eliminar o texto que está a seguir até ao fim da linha.

Mais uma forma de adicionar comentários:

/*
 Este é 
 um comentário
 de várias
 linhas 
*/

A linguagem C++ permite também fazer comentários por mais do que uma linha. Chama-se comentário por bloco e o que faz é eliminar tudo o que encontrar entre a sequência inicial /* e o final */.

A vantagem de termos esta possibilidade é poder comentar o nosso código. Existem algumas regras de boa conduta para comentários que foram criadas por pessoas que já têm muito tempo nisto:

  • Uma é criar logo no topo um comentário a dizer o que é o nosso programa, e o que faz numa forma geral;
  • Outra é fazer comentários a cada função que aparece no código a explicar;
  • Outra é comentar uma linha mais obscura, mais difícil de entender, que não é óbvia;
  • A outra é não comentar tudo. O comentar deve ser para sobressair.

Esta última regra pode ser esquecida quando o programa é didático, neste caso pode-se usar o programa como texto comentado.

Incluindo cabeçalhos

editar

#include <iostream>

O símbolo # é uma chamada de atenção ao compilador a dizer que aquela linha é para o preprocessador, depois temos o "include" (que basicamente diz para incluir código). Incluir o quê?

Deve incluir o ficheiro/arquivo iostream. (in+out+stream, "fluxo de entrada e saída", padrão) (na maioria das vezes, como entrada padrão temos o teclado e como saída temos o monitor) (este ficheiro/arquivo contém declarações das funções e definições que o nosso código fonte irá necessitar)

Este código que será incluído é chamado de cabeçalho devido a uma característica evidente, o fato de ser código de declaração inicial do programa, que deve estar no topo do arquivo/ficheiro.

Existem outros arquivos (ficheiros cabeçalho), o iostream é para fluxos de entrada e saída, mas temos muitos mais para matemática, manipulação de tempo, tratamento de caracteres, etc...

Na maioria das vezes, os arquivos de cabeçalho fazem parte de uma biblioteca. Podemos ver na parte dos anexos, algumas bibliotecas que existem juntamente com as funções de cada uma. Nós próprios podemos criar uma biblioteca com código e nosso próprio cabeçalho. E até podemos comprar bibliotecas existentes comercialmente, através de empresas especializadas em desenvolvimento, que também terão seus arquivos/ficheiros de cabeçalhos.

Mas, o que são bibliotecas? São arquivos com um conjunto de códigos que alguém fez antes. As que enunciamos antes são as "standard", são aquelas que têm as funcionalidades básicas, pertencentes aos padrões da linguagem. Repare-se que precisamos da biblioteca até para escrever (no ecrã)/(na tela) (stream + out) que nos permite utilizar o cout.

O ficheiro/arquivo iostream está envolvido em < >, isto significa que o preprocessador deve procurar o ficheiro/arquivo no sítio/diretório usual (que é onde o compilador usa como padrão para os "includes"). Se tivéssemos o ficheiro/arquivo iostream envolvido em "" significaria que o preprocessador deveria procurá-lo dentro de uma lista de diretórios de inclusão, "includes", iniciando pelo diretório atual.

As bibliotecas são compostas por 2 partes: um índice de todas as funções e definições e declarações, o cabeçalho, e depois a definição de todas as funções existentes no índice, arquivos de código.

As diretivas de preprocessamento não terminam com o ponto e vírgula como nas instruções.

Namespace

editar

using namespace std;

Observando esta linha, alguns tradicionais programadores em linguagem C, têm uma novidade: namespaces são espaços de nomes dentro do código, eles funcionam, entre outras coisas, como um meio de evitar duplicação de nomes dentro de um projeto extenso, que geralmente contam com inúmeros arquivos.

O C++ usa os namespaces para organizar os diferentes nomes usados nos programas. Cada nome usado no ficheiro/arquivo biblioteca "standard iostream" faz parte do "namespace" chamado de std.

O objeto de saída padrão, cout, está definido dentro do "namespace" std, ele é um objeto da classe "ostream" "output stream", para acessá-lo temos que referenciá-lo como "std::cout". Para evitar que tenhamos que informar "std::" todas as vezes que precisarmos usar os recursos deste "namespace", podemos informar que estamos usando-o dentro do arquivo atual, conforme vemos na linha declarada no início deste tópico.

O "namespace" permite que as variáveis sejam localizadas em certas regiões do código. Declarar o "namespace std" permite que todos os objetos e funções da biblioteca "standard input-output" possam ser usados sem qualquer qualificações específicas, desta maneira, não é mais necessário o uso de "std::".

Este é um conceito avançado que podemos explorar mais, vamos deixá-lo para depois.

Função "main"

editar

int main(){}

Como na linguagem C, a função principal de entrada do programa a partir do sistema operacional é a função main. Por isso mesmo ela é obrigatória em qualquer programa. Se não existisse uma "main function", não haveria entrada para que o sistema iniciasse o programa.

Todas as funções são declaradas e usadas com o operador ( ), assim é que o compilador reconhece que estas são funções. A ideia de ter funções é permitir o encapsulamento de uma ideia ou operação, dar um nome a isso e depois chamar essa operação de várias partes do programa simplesmente usando o seu nome. As funções declaradas como membros de uma classe de objetos podem ser chamadas de métodos.

Do ponto de vista funcional, um código dentro de uma função executa operações em outra parte do programa, que não é aquela de onde foi chamada, por este motivo as mesmas contam com um mecanismo de passagem de dados, ao declarar uma função indicamos quais os dados que entram e o que ela deve fornecer a quem lhe chamou. Pode-se dizer que, tal qual uma função matemática, a função em C/C++ poderá ser substituída, depois de sua execução, pelo valor que ela retorna, este valor será especificado antes do nome da função na declaração da mesma, conforme vemos no início deste tópico.

O int significa que a função vai retornar um inteiro. Existem outros tipos de dados como, por exemplo, os seguintes:

  • int que é a abreviatura de inteiro;
  • char que é a abreviatura de caratere;
  • float que é a abreviatura de "floating point number", ou seja, uma representação para número real.

Vejamos um exemplo:

Quando criamos uma função soma, obviamente só para ilustração pois isso não é necessário, podemos fazer:

int soma(int a, int b)
{ return a + b;
}

Agora imagine que tenhamos que somar 2 e 3, colocando o resultado em outra variável chamada valor, para isto faremos:

valor = soma(2, 3);

Primeiro analisamos qual é o resultado e depois substituímos a função pelo valor que ela retorna:

valor = 5;

Simples, não?

; - Final de sequência de instruções

O ponto e vírgula funciona como ponto final, separa as instruções e contextos. Repare que apenas as funções, ou melhor, as definições de funções e as diretivas de preprocessamento é que não têm o ";"

É importante notar que o código poderia ser todo escrito quase numa linha tipo:

int main (){int a; cout << "Hello world! Digite um número:\n"; cin >> a;cout <<
 "Você digitou o número: " << a<<"\n";return 0;}

É realmente o ";" que faz a terminação das instruções.

Ao encontrar as chaves "{}", o compilador reconhece como um delimitador de bloco, ou "body", corpo. O corpo de uma função ou bloco de código começa com "{" e termina com "}", como temos as instruções agrupadas, já não há necessidade de colocar o ";" no final para indicar onde é o fim do bloco.

No nosso exemplo existem 2 instruções no corpo da função. As instruções são executadas por ordem: do topo até ao fim a menos que existam funções que alterem o fluxo da leitura ou que existam códigos de controle de execução "execution control codes", que alteram o caminho de execução.

Entrada e saída (cin/cout)

editar

cout << "Hello world! Digite um número:\n";

(c+out) Podemos utilizar este objeto porque pusemos o header e o namespace std. As informações serão direcionadas através do iostream, um subsistema de entrada e saída da biblioteca padrão. O que este objeto nos permite é enviar o que temos entre aspas para a saída (out), que é o monitor neste caso.

Quem já conhece a linguagem C, certamente está familiarizado com os streams da biblioteca padrão, o stdin, o stdout e o stderr... A linguagem C++ implementa os mesmos dispositivos sob a forma de objetos.

O cout envia dados para o "standard output device", que é usualmente o monitor, a abstração do elemento de saída padrão é observada na presença de um objeto que representa a saída física de dados para o meio externo.

Observa-se que temos o operador <<, neste caso podemos verificar mais uma das funcionalidades da linguagem, pois este operador é usado para deslocamento de bits na sua funcionalidade padrão, neste caso a sua função foi substituída por outra, transferir os dados a sua direita para o "output stream" do seu lado esquerdo.

O cout é um objeto da biblioteca "standard C++" que tem como uma de suas funções imprimir strings no "standard output" (que normalmente é o/a ecrã/tela).

Da mesma forma que podemos formatar o texto enviado a saída padrão na linguagem C, também podemos fazê-lo com os objetos do C++, por exemplo, se acrescentássemos "<< hex <<" entre uma variável e a saída:

cout<< hex << n;

O resultado seria impresso em hexadecimal;

Para entrada de dados temos:

cin >> a;

O que esta linha faz é colocar o valor que foi digitado numa área de memória que foi chamada de "a".

Da mesma forma que o cout existe para saída de dados, temos outro objeto para entrada através do teclado, este objeto é chamado de Console IN - cin, seguindo a mesma analogia. Observe que o operador >> é usado para dar ideia de que os dados estão vindo do cin para a variável "a".

cout << "Você digitou o número: " << a << "\n";

Aqui voltamos a utilizar o objeto cout primeiro para imprimir no/na ecrã/tela a frase "Você digitou o número: ", depois vai buscar o valor que está naquela área de memória a que chamamos de "a" e por fim coloca o fim de linha através de "\n", em C++ podemos usar um finalizador de linha chamado endl, o uso do mesmo é mais eficiente pois descarrega os dados do stream logo após a finalização da linha.

função system("pause")

editar

system ("pause");

A maioria dos compiladores quando estão executando em modo gráfico fecha o console de saída assim que o programa finaliza. Isto impede que possamos ver o que aconteceu, principalmente quando o programa contém apenas umas poucas instruções.

A função system(), faz parte do padrão da linguagem C, ela executa uma chamada de sistema, ou seja, ela passa um comando para o sistema, que neste caso é "pause", como já informamos no início deste capítulo, este comando é destinado a sistemas da Microsoft®. Coloquei esta linha para que o programa não finalizasse sem que pudéssemos ver uma janela com o resultado, se não o fizesse a janela abriria e fecharia sem que pudéssemos ver o aconteceu durante a execução do programa.

Em sistemas como GNU/Linux, FreeBSD, Solaris®, etc... temos acesso a terminais de console e compiladores em linha de comando, assim basta compilar o programa sem esta linha e depois executá-lo, para ver o resultado.

Retornando valor

editar

return 0

Faz com que a função retorne o valor zero, como esta função é a principal do programa, por onde o sistema operativo/operacional iniciou a execução do mesmo, este retorno é recebido pelo sistema, é comum que valores diferentes de zero sejam interpretados como erro do programa.

Esta instrução manda retornar o valor zero para o sistema operativo/operacional (Windows, Unix, ...). Este zero representa a dizer que o programa finalizou normalmente. Pode acontecer que o programa não finalize como seria de esperar, ele tem um crash (ou porque ficou com falta de memória.). O sistema operativo/operacional necessita de lidar com estas terminações anormais de uma forma diferente das normais. Por isso é que o programa diz ao sistema operativo/operacional que terminou normalmente.

Questão: porque é que o sistema operativo necessita de saber que o programa terminou bem?

Constantes

editar

Compatível com a linguagem C, o C++ mantém as constantes básicas e introduz algumas novas funcionalidades possibilitadas pelo modificador const.


O uso do modificador const tem duas funções principais:

  1. Resguarda da inviolabilidade de valores apontados por ponteiros;
  2. Auxílio na compreensão das características de funções, durante a implementação.


Simbólicas

editar

Constantes simbólicas podem ser criadas com as diretivas do preprocessador #define. Neste modo os valores, de fato, não são interpretados imediatamente pelo compilador, antes são identificados e substituidos pelo preprocessador no estágio anterior à compilação. Por exemplo:

#define BUFFER_LENGTH 2048

...
...
...

char data[BUFFER_LENGTH];

Observe que o valor 2048 será usado logo abaixo no código, depois que o preprocessador substituir a constante simbólica BUFFER_LENGTH pelo valor que lhe foi atribuído.

Note que as constantes são escritas com todas as letras maiúsculas, isso não é uma regra, mas ajuda a identificar o que é constante simbólica dentro do programa, sendo adotado pela maioria dos desenvolvedores como uma boa prática de programação.

Neste caso, podemos definir valores simbólicos compostos, por exemplo:

#define BUFFER_LENGTH 2048
#define N_BUFFERS 100
#define MASTER_LENGTH ( BUFFER_LENGTH * N_BUFFERS )
...
...
...

char screen[MASTER_LENGTH];

Os valores podem ser simbólicos em formato de código, o que permite criar programas com melhor legibilidade. Para isso podemos colocar expressões com funcionalidades bem definidas substituídas por nomes que as identifiquem. Por exemplo:

float a[3];

#define PRINT_VECTOR cout << a[0] << " , " << a[1] << " , " << a[2] << endl
...
...

PRINT_VECTOR;

Desta forma, todas as vezes que quisermos mostrar o valor do vetor de três coordenadas podemos usar a constante PRINT_VECTOR.

Literais

editar

Constantes literais podem ser declaradas da mesma forma que na linguagem "C", ou seja, podemos definir valores fixos em qualquer parte do programa, expressando-os diretamente no código através de seu valor significativo. Por exemplo, podemos definir números:

256 //decimal
0400 //octal
0x100 //hexadecimal

Também podemos definir valores para caracteres ou cadeias de caracteres, como segue:

'a'             // um caractere
"abc"           // uma cadeia de caracteres
"\xF3\x23\x12"  // uma cadeia de caracteres representada por seus valores em hexadecimal

Temos ainda a possibilidade de declarar constantes compostas por valores e operadores:

(4.23e14 * (12.75 + 12976.18/36)) // constante composta

Enumerações

editar

Valores enumerados são muito recorrentes nos ambientes de programação, por isso podemos contar com a declaração de enum em C++ também, o que segue a mesma sintaxe que temos em "C":

enum seq {A,B,C,D};

seq x;

ou ainda:

enum nomes {LANY=100,SANDRA=200,MARCIA=300,RODRIGO=400};

nomes x;

Porém, observamos uma diferença: a palavra enum pode ser dispensada na declaração da variável, enquanto que em C é obrigatório,apesar desta pequena diferença a funcionalidade do recurso é a mesma, ou seja, pode-se definir variáveis que assumem estritamente os valores presentes na declaração de enumeração.

Este recurso torna-se útil na padronização de valores a serem usados como entrada de funções, por exemplo. Pode ser considerada como uma funcionalidade mnemônica, seu uso não altera o código final caso modifiquemos o programa para que use variáveis inteiras ou strings de mesmo valor do enum.

A seguinte sintaxe:

seq x = 3;

Não é permitida, mesmo que o valor presente no enum seja avaliado como 3 pelo compilador em tempo de compilação. Isso pode parecer confuso, mas lembre-se de que os valores serão atribuidos pelo compilador, logo isso evita que o mesmo programa seja compilado em ambientes diferentes e tenha comportamento diferente.

Variáveis

editar

As variáveis no C++ podem ser usadas da mesma forma que na linguagem "C", porém algumas poucas diferenças podem ser destacadas, principalmente aquelas que trazem à linguagem C++ características próprias da orientação a objetos.

Como na linguagem "C", os tipos nativos do compilador em uso são referenciados por:

 char
 int
 float
 double

Que correspondem a números com tamanho relativos, com os significados respectivos: caractere, inteiro, ponto flutuante e ponto flutuante de dupla precisão. De qualquer forma a extensão dos mesmos depende da máquina que se pretende programar. Considerando que nem sempre teremos que programar apenas computadores, poderemos ter extensões bem distintas dependendo do hardware a ser programado, por exemplo, computadores domésticos tipicamente tem processadores de 32 ou 64 bits hoje em dia, enquanto que dispositivos embarcados podem ter processadores de 8, 16 ou 32 bits. Portanto, o compilador para cada caso atribui faixas diferentes para cada tipo em cada situação.

A linguagem C++ introduz o tipo bool, que representa o valor booleano, falso ou verdadeiro, o que não existe na linguagem "C", porém seu tamanho na memória depende da capacidade de otimização do compilador usado. Tipicamente os compiladores para computadores usam uma variável do tamanho de char para representar o valor, o que poderia ser considerado um desperdício, mas devido à abundância de memória não chega a ser inadequado. Porém em sistemas pequenos há compiladores que armazenam o valor booleano em apenas um bit. Obviamente, se o processador possuir recursos de manipulação de bits isso é muito útil e pode ser usado como um fator de melhoria da qualidade do software desenvolvido. Em outros ambientes, onde a manipulação de bits traga prejuízo para o desempenho usa-se a estratégia padrão de desperdiçar um pouco de espaço em favor de uma agilidade maior nas operações. Portanto, embora as variações de utilização do espaço sejam muitas, o compilador sempre fará a mais apropriada para cada ambiente de utilização da linguagem.

Modificadores

editar

O C++ conta com os modificadores de amplitude (short,long) presentes na linguagem "C" e modificadores de acesso, alguns exclusivos do C++, que estão diretamente ligados a características da POO (programação orientada a objetos). Desta forma descreveremos apenas os tipos relevantes exclusivamente para a programação na linguagem escopo do livro presente sem nos aprofundarmos na teoria por trás dos mesmos. A prática do uso dos mesmos é melhor indicada como meio de aprofundamento do tema.

Assim contamos com os modificadores da linguagem "C":

static
short
long
unsigned 
signed

A linguagem C++ introduz um novo modificador chamado const, que tem comportamento variado dependendo do local onde está sendo declarado. Sua função, basicamente, é estabelecer um vínculo entre declaração e obrigatoriedade da coerência no uso do símbolo declarado.

A princípio, quando declaramos uma constante com este modificador fazemos com que seja obrigatório o uso do símbolo de forma que o mesmo não possa ter seu valor alterado. Assim, se fizermos:

const int x = 4;

O inteiro x não poderá deixar de ter valor igual a 4. Qualquer tentativa de modificar o valor da constante ao longo do programa será reportada como erro pelo compilador. Porém podemos considerar esta funcionalidade como óbvia e trivial, ainda temos o uso do modificador de uma forma mais proveitosa, na passagem de parâmetros para funções, por exemplo:

void inhibitX(const int *x)
{
...
...
 BASEADDRESS = z*((*x) - 23p*71);
...
...
}

Neste caso, a função acima recebe um valor inteiro através de um ponteiro, que não obrigatoriamente precisa ser constante no escopo fora da função, porém dentro da mesma a variável será constante. Fazendo este simples procedimento teremos como fazer com que um símbolo seja variável fora da função e constante dentro da mesma, de forma que dentro do escopo da mesma só façamos leituras do seu valor. O artifício cria duas consequëncias importantes: a primeira é a melhor legibilidade do código, visto que ao usarmos uma função teremos certeza de que os valores não serão alterados dentro da função; a segunda é que poderemos evitar erros inadvertidos de atribuição de valores à variável quando da construção da função.

volatile

editar

Uma variável "volátil", como a própria expressão sugere, é uma variável que pode ser modificada sem o conhecimento do programa principal, mesmo que esta ainda esteja declarada dentro do escopo onde o programa está sendo executado. Isso está relacionado, principalmente a processos concorrentes e "threads", estes podem alterar o conteúdo da variável em eventos fora da previsibilidade do tempo de compilação. Em outras palavras, o compilador não pode prever com segurança se pode otimizar trechos de programa onde esta variável se encontra.

A palavra reservada volatile é destinada as situações onde uma variável pode ter seu valor alterado por fatores diversos, e portanto, não pode ser otimizada. Usando-a o programador informa ao compilador que não deve interferir na forma com que o programa foi escrito para acesso a esta variável. Desta forma impede que erros inseridos por otimização estejam presentes na versão final do executável.

O uso desta palavra implica em mudança no comportamento do compilador durante a interpretação do código. As classes de objetos do tipo voláteis só poderão ser acessadas por rotinas que declarem aceitar como entrada dados voláteis, da mesma forma que apenas objetos voláteis podem acessar variáveis voláteis. Esta "amarração" faz com que o uso de tais variáveis se torne mais seguro.

Podemos declarar variáveis voláteis da seguinte forma:

volatile int x;

Enquanto que para funções que acessam tais variáveis teremos consequências visíveis na montagem do código, por exemplo, se tivermos o seguinte trecho de programa:

int x = 1265;

void main_loop()
{
  while( x == 1265)
      {
       // fazer alguma coisa
      }
}

Poderemos ter uma otimização gerada pelo compilador como segue:

int x = 1265;

void main_loop_optimized()
{
  while( true )
      {
       // fazer alguma coisa
      }
}

Considerando que em um programa que foi desenhado para ambiente multitarefa isso não pode ser considerado verdadeiro, pois o programa pode estar esperando que uma das tarefas modifique o estado da variável para prosseguir seu curso, a otimização acima será um desastre, uma vez que a função acima jamais será encerrada.

Para evitar isso fazemos:

volatile int x = 1265;

void main_loop()
{
  while( x == 1265)
      {
       // fazer alguma coisa
      }
}

E o compilador não poderá mais avaliar que o valor de x pode ser otimizado para o valor corrente, pois informamos na declaração que o valor da variável pode ser alterado sem seu conhecimento. Desta forma o mesmo não alterará o algorítmo e fará o teste da variável dentro do while.

Nomeando tipos

editar

A linguagem "C" possui recursos de nomeação de tipos simples e compostos através das palavras chaves typedef e struct. Adicionada a estas o C++ acrescenta a palavra chave class. Vejamos como devemos definir um novo tipo através desta palavra chave.

A palavra class atribui a um conjunto de tipos de dados o estado de modelo de objeto. Este conceito é fundamental para o modo avançado de programar usando o C++. Com este identificador declaramos objetos, da mesma forma que declaramos estruturas.

Uma classe pode ser definida em um cabeçalho "header", da seguinte forma:

class nome_da_classe
{ <tipo_1> variavel_1;
  <tipo_2> variavel_2;
  .
  .
  .
  <tipo_n> variavel_n;
  -----
  <tipo_n> nome_funcao ( <tipo_1> variavel_1, <tipo_2> variavel_2, <tipo_3> variavel_3 ...);
};

O mais interessante de observar é a presença de uma função dentro da declaração acima. Então poderíamos perguntar: Por que colocar uma função dentro de um tipo? A resposta é simples: Para manipular os dados dentro do tipo! Não apenas por esta característica, mas por várias outras que abordaremos nos capítulos seguintes, o tipo class é extremamente flexível e poderoso.

É importante ressaltar que em C++ a declaração do identificador: enum, struct, class, etc... é dispensado quando se declara uma variável ou objeto para o referido tipo. Desta forma podemos ter também as seguintes declarações como válidas, além do uso padrão da linguagem "C":

struct data{ int a; 
             int b; 
             int c; 
           };

class object{ int a;
              char b; 
              long w;
              float p;
              void getData();
            };

...
...
...
...

void func()
{ data x;
  object y;
  ...
  ...
  y.getData();
}

Como podemos ver na função acima se a variável x for declarada para uma estrutura data o uso da palavra struct não é obrigatório, assim como também não o é para outros tipos de dados compostos.

Ponteiros

editar

Em linguagem "C", podemos definir variáveis ponteiro, ou seja, variáveis que armazenam o endereço de outras variáveis. Este recurso é largamente explorado pela linguagem, embora que deva ser usado com cautela por iniciantes devido ao seu poder destrutivo. Como linguagem "irmã mais nova" o C++ também permite o uso de ponteiros, o que a distingue de muitas outras linguagens orientadas a objeto. Embora seja muito difundida a ideia da criação de linguagens que não suportem acesso a ponteiros, basicamente pressupondo que todos os programadores são inexperientes, a falta deste recurso limita as capacidades de interação de programas com o hardware. Em outras palavras, a falta de um meio de manipular ponteiros faz a linguagem limitada ou dependente de fabricantes de bibliotecas que acessem o hardware.

A disponibilidade do uso de ponteiros em C++ agrega um poder a mais ao conjunto da linguagem, porém implica a necessidade de cautela na elaboração de programas que usam deste recurso. Certamente, nem todos os programadores precisam ser considerados inaptos, a priori, através da supressão ou inserção de complicadores de recursos criados explicitamente para forçá-los a não usar dos recursos. Por isso, a linguagem C++ disponibiliza o recurso para quem deseja utilizá-lo e também apresenta diversos outros recursos que são alternativas ao uso de ponteiros quando eles não são imprescindíveis.

O operador *

editar

O operador *, chamado de apontador, funciona em C++ da mesma forma que em C. Considerando que tenhamos uma variável ponteiro p:

  • Em p armazena-se o endereço de memória que queiramos manipular. Na maioria das vezes obtemos o endereço de outra variável e colocamos em p;
  • Se p é um ponteiro, *p é o valor apontado por p, ou seja, o valor que está armazenado no endereço de memória que queremos ler ou alterar.

Na declaração de variáveis, uma variável declarada com * é um ponteiro.


Exemplo:

           int *px;


Muitas vezes, iniciantes podem se sentir confusos porque quando declaramos um ponteiro usamos o * e quando atribuímos endereços a ele não usamos o *. A conceituação básica é a seguinte:


  • Declaramos o ponteiro com *, para que o compilador identifique que a variável é um ponteiro;
  • Usamos o ponteiro sem *, para acessar o endereço que ele aponta na memória;
  • Usamos o ponteiro com *, para acessar o valor do dado armazenado na posição de memória;

O operador &

editar

Na linguagem "C", o operador & tem três funções básicas, funciona como operador da função lógica AND como operador binário bit-a bit AND e como operador de leitura de endereços. Para operações com vetores, isso é usado da seguinte forma:

  int a = 12;
  int *pa;
  ...
  ...
  pa = &a;
  ...
  ...
  *pa = 100;

Ou seja, declaramos a variável a, depois declaramos um ponteiro pa, através do operador & obtemos o endereço de a e atribuímos o valor 100 à variável usando o ponteiro ao invés da variável a. Desta forma alteramos o valor de a indiretamente.

Um outro uso de & (que não tem similar em "C") pode ser visto mais adiante, em Referências de dados, mas, para isto, é necessário estudar o que são Funções.

O ponteiro "this"

editar

Imagine que tenhamos criado um objeto qualquer de uma classe X, se quisermos ter acesso ao ponteiro que contém a posição de memória onde está armazenado este objeto basta chamar o ponteiro "this". O ponteiro "this" é uma das características dos objetos em C++ e algumas outras linguagens que suportam orientação a objetos. Ele é um membro inerente a todos os objetos que instanciamos em programas escritos em C++.

Faremos uma breve explanação a respeito de objetos para esclarecer este tópico. Objeto são parecidos com estruturas, uma diferença básica é que estes possuem "habilidades específicas" representadas por funções que estão dentro do seu escopo. Vejamos um exemplo:

struct Data
{ int x,y;
  int get_x(){ return x;}
  int get_y(){ return y;}
  int set_x(int a){ return x=a;}
  int set_y(int b){ return y=b;}
};

Observe que a estrutura acima apresenta dois inteiros e duas funções para cada um deles, uma que atribui o valor e outra que lê o valor de uma das mesmas. Detalhes das implicações a respeito desse modo de operar os valores serão dados nos capítulos seguintes que tratam de objetos. Por ora vamos nos ater a um conceito fundamental importante para a noção de ponteiros em C++, a identidade de um objeto.

Veja, temos uma estrutura de dados que está na memória, os dados estão lá (variáveis x e y), porém as funções não estarão lá, pois se tivéssemos que copiar uma função para cada estrutura que criássemos o programa tomaria um tamanho monstruoso. O que se faz é apenas guardar o endereço da estrutura em um ponteiro especial, o ponteiro this. Assim, o compilador poderá criar uma única cópia de função para todas as estruturas que criarmos e depois quando a função quiser manipular os dados de uma estrutura em particular, o fará através do ponteiro this.

Examinemos os detalhes mais de perto... Digamos que instanciemos um objeto "A" da classe Data:

 Data A;

 A.set_x(2);
 A.set_y(7);

Para acessar estas funções o compilador fará:

 Data A;

 A.set_x(2);
// { Data *this = &A;
//  return this->x = 2;
// }
 A.set_y(7);
// { Data *this = &A;
//  return this->y = 7;
// }

Desta forma podemos perceber como diferentes conjuntos de dados podem ser manipulados pela mesma função. Quando declaramos uma função dentro de uma estrutura de dados esta rotina recebe um ponteiro com o endereço do conjunto de dados que deve tratar toda vez que for invocada pelo programa. Assim, sempre acessará os dados através deste ponteiro, o this. Como todos os objetos precisam ser identificados por esse ponteiro, ele é definido para qualquer objeto com o mesmo nome: this.

Vetores e Matrizes

editar

Façamos uma pequena revisão de conceitos:

  • Vetores e matrizes são variáveis compostas homogêneas, ou seja, são agrupamentos de dados que individualmente ocupam o mesmo tamanho na memória e são referenciados pelo mesmo nome, geralmente são individualizadas usando-se índices.
  • Vetores distinguem-se das matrizes apenas pela característica de ter dimensão (1 x n) ou (n x 1), essencialmente vetores são matrizes linha ou matrizes coluna.

Em linguagem "C" vetores e matrizes são usados abundantemente para compor estruturas de dados necessárias para composição de diversos recursos. Esta usa, mais explicitamente, vetores de caracteres para definir cadeias de texto, o que é conhecido como o mais trivial uso de vetores. Além deste recurso, o "C" também define meio de criação de matrizes tipo (n x m), provendo, desta forma os recursos necessários para criação destes conjuntos de dados.

A linguagem "C++" suporta os mesmos recursos e permite a criação de matrizes de objetos. Uma vez que um objeto é essencialmente um tipo de dado criado pelo programador, todas as características básicas legadas aos "tipos" em geral são observados nos tipos criados (classes de objetos).

Vetores

editar

Os vetores em C++ seguem a mesma notação da linguagem "C", via de regra declara-se o tipo seguido de um asterisco. Para acessar o valor apontado pela variável usa-se um asterisco de forma semelhante, como pode ser visto no trecho de código abaixo:

int *x;
int a = 3;

x = &a;

cout <<" O valor do conteúdo da posição 0x";       // O valor da posição 0x23A0209112 
cout << hex << x << "de memória é " << *x << endl; // de memória é 3

Matrizes

editar

Podemos imaginar que uma matriz é um conjunto de vetores que ocupam uma determinada área de memória referenciada por um nome comum. Matrizes de tipos primitivos são conseguidas através de associações do operador [ ], como por exemplo:

char A[32][16];
int B[12][26][10];

Definindo nossas classes de objetos poderemos declarar matrizes de objetos:

class Record
{ int D;
  float X,Y;
  char desc[12];

  public:
   Record();

   void addFData(float A, float B);
   float getFDataX();
   float getFDataY();
   ...
   ...
   ...
};

void function()
{
 Record A[32][16];
 ...
 ...
 ...

Ou seja, podemos adotar a mesma sintaxe para criar matrizes de objetos. Este procedimento pode ser usado com o cuidado de se avaliar antes a quantidade de memória disponível para que a matriz não ultrapasse esse limite físico, muitas vezes delimitada pelo hardware ou pelo sistema operacional. Lembremos que, um objeto precisa do espaço equivalente a soma de suas variáveis internas para ser alocado na memória.

Declarando arranjo

editar

Os arrays permitem fazer o seguinte:

int a1, a2, a3,….a100; 	é equivalente a ter 	int a[100];

Ou seja permite declarar muitas variáveis de uma forma bem simples, poupa escrita e é bastante compreensível.

  • O número que está dentro de brackets [] é o size declarator. Ele é que vai permitir ao computador dizer quantas variáveis a serem geradas e logo quanta memória deverá ser reservada. A memória reservada para as variáveis vão ser todas seguidas, um int a seguir ao outro
  • Há uma forma para não dizer o valor deste size declarator, mas isso apenas acontece antes da compilação, ou seja o compilador é que vai fazer esse preenchimento por nós, visualizando o nosso código e contanto os membros que colocámos. Isto é um automatismo dos compiladores recentes. chama-se a isto iniciação implícita que vamos ver nesta secção.
  • As variáveis geradas pelos arrays vão ser todos do mesmo tipo.
  • Reparem que o valor do size declarator é um número. É literal, ele não vai mudar quando o programa estiver a correr. Por isso quando não soubermos o número de elementos o que fazemos?

Veja uma tentativa:

 #include <iostream>
 using namespace std;
 int main ()
 {
   int numTests;
   cout << "Enter the number of test scores:";
   cin >> numTests;
   int testScore[numTests];
   return 0;
 }

Isto vai dar um erro de compilação, porque o array está a espera de uma constante e não uma variável. Há uma maneira de contornar isto que é através da memória dinâmica que vamos dar mais tarde, num capitulo próprio, pois isto vai envolver muitos conceitos.

Constantes

editar

Reparem que há uma diferença entre literais e constantes, apesar de em ambos os casos o valor não é alterado durante a execução do programa, a constant é um nome que representa o valor, o literal é o valor.

Declarar constantes

editar

É exatamente como declarar uma variável com duas diferenças:

  1. A declaração começa com a palavra const. Isto vai dizer ao compilador que é uma constante e não uma variável
  2. Teremos de atribuir logo o valor na declaração, ou seja, é fazer a iniciação

Exemplo:

const int numTests = 3;

Portanto se tentarmos colocar um outro valor ao numTest, isso vai dar um erro de compilação

Array index

a[100] é composto por a[0], a[1],… a[99] ( De a[0], a[1],… a[99] existe 100 posições)

Pergunta: Porque é que o índex começa em zero e não um? Ou seja temos as 100 posições que pedimos mas o índex começa no zero e não no 1. A razão disto tem a ver com offset – que refere ao valor adicionado para o endereço base para produzir a segunda address. Bem não entendi bem! Eu explico de novo: O endereço (address) do primeiro elemento do array, é o mesmo do que o endereço base do próprio array. ah espera aí, o que estão a dizer é que o endereço do array é igual ao do primeiro elemento do array. Assim o valor que teria de ser adicionado, ao endereço base do array, para conseguirmos o endereço do primeiro elemento seria zero. Agora sim, percebi!

Erro: Um erro comum é esquecer que o index começa no zero, e portanto quando se querem referir ao último elemento, esquecem-se que têm de subtrair uma unidade. O que advém desse esquecimento é que podem estar a alterar memória pertencente a uma variável, instrução,..de um outro programa. – Ou seja vai existir violação de dados.

Se o array for declarado globalmente em vez de ser localmente, então cada elemento é inicializado ao valor defaut que é zero.

Iniciação

editar

Iniciação, se bem se recordam é atribuir um valor a uma variável ao mesmo tempo que declaramos a variável. Podemos fazer a iniciação de um array de 2 maneiras:

1) explicit array sizing

int testScore[3] 	= { 74, 87, 91 };
float milesPerGallon[4] = { 44.4, 22.3, 11.6, 33.3};
char grades[5] 		= {'A', 'B', 'C', 'D', 'F' };
string days[7] 		= {"Sunday", "Monday", "Tuesday", "Wednesday","Thursday", "Friday", "Saturday"};

Pergunta: O que é que acontece se dermos mais valores de atribuição do que elementos do array?

int a[3] = { 74, 87, 91, 45 };

Isto vai dar um erro de compilação “too many initializers”

Pergunta: O que é que acontece se tivermos menos valores do que os elementos?

int a[3] = { 74 };

Não acontece nada simplesmente não temos valores para a[1] e a[2]. Porém em alguns compiladores os elementos não inicializados ficam com os valores defaut, que no caso dos ints é 0 no caso dos floats é 0.0 e nos caracteres é o caractere nulo ("\0"). No entanto se não inicializarmos um dos elementos, os restantes elementos terão de ser não inicializados pois caso contrário teremos um erro de compilação


2) implicit array sizing

int testScore[ ] 	= { 74, 87, 91 };
float milesPerGallon[ ] = { 44.4, 22.3, 11.6, 33.3};
char grades[ ] 		= {'A', 'B', 'C', 'D', 'F' };

Aqui o compilador faz o trabalho por nós, conta os elementos e preenche o número de elementos

Caracter array

editar
char name[ ] 	= {'J', 'e', 'f', 'f', '\0' };
char name[ ]	= "Jeff";

Ambas as inicializações são permitidas. Porém tomar atenção á ultima iniciação! Quando colocámos as aspas duplas o compilador acrescenta o "\0" na array que cria! Não tem []!! Esta até costuma ser a preferida pelos programadores, é ao estilo de strings (Na verdade as strings são arrays de char mas vamos falar disso num capitulo próprio)

O char "\0" é o escape sequence para caracterer null. Este escape sequence sinaliza ao cout o fim do character array. É o último elemento do array preenchido! Se não tivéssemos este carácter apareceriam estranhos caracteres a seguir ao "jeff", chamados "caracteres-lixo". (porquê?) Isto não significa que o último elemento deva ser sempre o null carácter

Arrays de várias dimensões

editar

Podemos ter duas dimensões

tipo_da_variável nome_da_variável [altura][largura];

como também poderíamos ter infinitas

tipo_da_variável nome_da_variável [tam1][tam2] ... [tamN];


Iniciando

editar
float vect [6] = { 1.3, 4.5, 2.7, 4.1, 0.0, 100.1 };
int matrx [3][4] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 };
char str [10] = { 'J', 'o', 'a', 'o', '\0' };
char str [10] = "Joao";
char str_vect [3][10] = { "Joao", "Maria", "Jose" };

Peguemos no exemplo:

int a [2][3]={1,2,3,4,5,6,}


Na memória teríamos as coisas assim. ou seja os elementos são seguidos e do mesmo tipo

a[0][0] a[0][1] a[0][2] a[1][0] a[1][1] a[1][2]
1 2 3 4 5 6

Portanto ter int a [2][3] é equivalente a ter int a [6] o nome que se dá é que é diferente.

Pergunta: será pedido espaço par 6 ints ou antes um espaço com o tamanho de 6 ints? Como nós sabemos que os arrays os elementos têm endereços de memoria consecutivos, por isso, não podem ser pedidos 6 ints, pois se fosse esse o caso, poderia acontecer que eles não ficassem juntos.

Const Constant arrays

editar
const int daysInMonth [] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

Recordar que temos de inicializar quando queremos fazer uma constante array

Atribuir valores ao array

editar
#include <iostream>
using namespace std;
int main ()
{
  int testScore[3];
  cout << "Enter test score #1: ";
  cin >> testScore[0];
  cout << "Enter test score #2: ";
  cin >> testScore[1];
  cout << "Enter test score #3: ";
  cin >> testScore[2];
  cout << "Test score #1: " << testScore[0] << endl;
  cout << "Test score #2: " << testScore[1] << endl;
  cout << "Test score #3: " << testScore[2] << endl;
  return 0;
}

Podemos atribuir o valor 1 a 1, mas para poupar escrita de programação é melhor utilizar as funções anteriormente revistas como o for

#include <iostream>
using namespace std;
int main ()
{
  int testScore[3];
  for (int i = 0; i < 3; i++)
  {
     cout << "Enter test score #" << i + 1 << ": ";
     cin >> testScore[i];
  }
  for (i = 0; i < 3; i++)
  {
     cout << "Test score #" << i + 1 << ": " << testScore[i] << endl;
  }
  return 0;
}

Ou melhor ainda podemos usar uma constante, para tornar o nosso código mais abstracto.

#include <iostream>
using namespace std;
const int MAX = 3;
int main ()
{
   int testScore[MAX];
   for (int i = 0; i < MAX; i++)
   {
      cout << "Enter test score #" << i + 1 << ": ";
      cin >> testScore[i];
   }
   for (i = 0; i < MAX; i++)
   {
      cout << "Test score #" << i + 1 << ": " << testScore[i] << endl;
   }
   return 0;
}


Lembram-se da história de termos o endereço do array igual ao endereço do 1º elemento do array?

#include <iostream>
using namespace std;
const int MAX = 3;
int main ()
{
   int testScore[3] = { 74, 87, 91 };
   cout <<   testScore[0] <<"\n";
   cout <<   testScore <<"\n";		//array base e não um elemento particular do array
   return 0;
}

Pois bem vemos que quando mandamos imprimir o array, ele dá um endereço. Pois o valor do nome do array é o endereço do array.

Arrays como statements de funções

editar

Pegando no programa

#include <iostream>
using namespace std;
const int MAX = 3;
int main ()
{
  int testScore[MAX];
  for (int i = 0; i < MAX; i++)
  {
     cout << "Enter test score #" << i + 1 << ": ";
     cin >> testScore[i];
  }
  for (i = 0; i < MAX; i++)
  {
     cout << "Test score #" << i + 1 << ": " << testScore[i] << endl;
  }
  return 0;
}

Vamos torná-lo mais modular, escrevendo uma função para atribuir valores ao array e outa função para mostrar os valores do array

#include <iostream>
using namespace std;
void assignValues(int[], int);
void displayValues(int[], int);
const int MAX = 3;
int main ()
{
  int testScore[MAX];
  assignValues(testScore, MAX);
  displayValues(testScore, MAX);
  return 0;
}
void assignValues(int tests[], int num)
{
  for (int i = 0; i < num; i++)
  {
     cout << "Enter test score #" << i + 1 << ": ";
     cin >> tests[i];
  }
}
void displayValues(int scores[], int elems)
{
for (int i = 0; i < elems; i++)
  {
     cout << "Test score #" << i + 1 << ": "<< scores[i] << endl;
  }
}


Arrays como argumentos de funções

editar
// arrays as parameters
#include <iostream>
using namespace std;
void printarray (int array[], int length) /*função com 2 argumentos,um deles é um array */
{
 for (int n=0; n<length; n++)
     cout << array[n] << " ";
 cout << "\n";
}
int main ()
{
 int a[] = {5, 10, 15};
 printarray (a,3);     //passo array como argumento
 return 0;
}

Este exemplo por acaso está muito curioso

Pergunta: mas agora deveríamos perguntar se neste caso tínhamos uma passagem por valor ou referência.

Quando um array é passado para uma função, a função recebe não a cópia do array mas invés disso o endereço, address do primeiro elemento do array, que é igual ao valor do array (base).

Assim todas as modificações que se efectuarem na função que foi chamada irão repercutir-se no array passado.

Vamos confirmar:

 #include <iostream>
 using namespace std;
 void doubleThem(int a[], int size);
 int main()
 {
    int a;
    int myInts[10] = {1,2,3,4,5,6,7,8,9,10};
    doubleThem(myInts, 10);	//passei o array base
    for (a=0; a<10; a++)
    {
        cout << myInts[a] <<”\t;
    }
    return 0;
 }
 void doubleThem(int a[], int size)
 {
    int i;
    for (i = 0; i < size; i++)
    {
        a[i] = 2 * a[i];
    }
 }

Breve revisão

editar

Conceito

editar

Da linguagem "C" também temos o conceito de estrutura, do qual faremos uma pequena revisão agora. Como todos sabemos, nem todos os dados que precisamos usar podem ser agrupados em matrizes. Frequentemente usamos dados de diversos tipos diferentes, com tamanhos diferentes. Para tipos de dados de diferentes tamanhos existem estruturas de armazenamento de dados heterogêneos.

O especificador struct é usado para esta finalidade. Com ele podemos criar tipos que armazenam dados compostos por agrupamentos de outros tipos primitivos da linguagem. Geralmente, os dados são armazenados de forma a facilitar a identificação de cada campo de dados que pretende-se manter, para isso usamos nomes para cada campo dentro da estrutura de dados, de forma a ter um meio de acessá-la depois.

Estruturas são blocos básicos de informação e são manipulados de maneira primitiva. Basicamente o compilador instrui a montagem de um código que manipula-as de forma a copiar, referenciar e obter posição na memória. Todas as outras formas de tratar os dados devem ser providas pelo código do programa.

Implementação

editar

Para criar um tipo de dados composto heterogêneo, basicamente, cria-se uma lista de tipos e nomes de variáveis separadas por ponto e vírgula. Podemos imaginar esta lista como um bloco de código, em linguagem C, onde estão presentes apenas as declarações de variáveis. Para isso temos a seguinte sintaxe:

struct Estrutura
{
  <Tipo A> NomeVariavelA;
  <Tipo A> NomeVariavelA2;
  <Tipo A> NomeVariavelA3;
  <Tipo B> NomeVariavelB;
  <Tipo C> NomeVariavelC;
  <Tipo D> NomeVariavelD;
  ...
  ...
  <Tipo Z> NomeVariavelZ;


} [<NomeVariavelComposta>];

O nome da variável composta pode ser omitido na declaração da estrutura e depois definido onde for mais apropriado, geralmente, dentro de algum bloco de código onde venha a ser usado. Podemos ver logo abaixo, o exemplo de uma declaração de estrutura:

struct Estrutura
{
  int Inteiro;
  double PontoFlutuante;
  char Caracteres[10];
};

int main()
{
  Estrutura MinhaVariavelComposta;
  ...
  ...
  ...
  return 0;
}

Acessando dados internos

editar

O modo de acesso a variáveis internas de uma estrutura é feito através do operador ponto ".", porém, quando usamos ponteiros para guardar o endereço de uma estrutura usamos o operador seta "->". Vejamos um pequeno trecho de código:

   Estrutura st;
   Estrutura *pst;
   
   st.Inteiro = 200;

   pst = &st;
   
   pst->PontoFlutuante = 23.976;

Estruturas em C++

editar

As estruturas em C++ funcionam de modo análogo ao apresentado em linguagem "C". A diferença, a princípio, notável entre elas nas duas linguagens é que em "C++" o especificador struct não precisa ser escrito quando criamos a estrutura:

Em C, para criar uma estrutura de dados chamada st, declaramos:

  struct Estrutura st;

Para fazer o mesmo em C++, declaramos:

  Estrutura st;


Este simples detalhe revela uma característica importante das estruturas em C++: Nesta linguagem as estruturas são tratadas como tipos primitivos de objetos. Elas têm características semelhantes às classes, que veremos nos capítulos subsequentes.

As estruturas em C++ também podem conter funções além de dados. Este fato vem da ideia de modelo de objeto que as estruturas mantém nesta linguagem. Objetos devem ter propriedades (variáveis) e métodos (funções membro), por isso temos a possibilidade de criar funções dentro do corpo das estruturas, com a finalidade de lidar com os dados que elas mantém.

Construtores

editar

Os construtores são funções que são criadas automaticamente sempre que tentamos criar um objeto. Eles funcionam da mesma maneira que construtores de classe. A esses que são criados automaticamente são os chamados de defaut.

Se escrevermos o código:

#include <iostream>
#include <string>
using namespace std;

 struct Person 
 {
   string name; 
   float height;
 };

 int main ()
 {
   Person p1;
   cout << "O nome da pessoa é " << p1.name << " e a altura é " << p1.height << endl;
   cin.get();   
   return 0;
 }

O resultado é:

O nome da pessoa é e a altura é 1.75+e2783

Aqui é criado um construtor defaut no momento em que criamos a instância p1 ex com a linha Person p1;

Como as variáveis membro não foram iniciadas, o valor de name está vazio e o na variável height está um valor qualquer – que é lixo!

Construtor sem argumentos

Podemos ter um construtor sem argumentos que ao contrário do construtor defaut designa valores padrões às variáveis membro.

 struct Person 
 {
   string name; 
   int height;
   Person()		//construtor sem argumentos
   {
      name = "Nenhum nome assinalado";
      height = -1;
   }
 };
  • O nome do construtor é sempre igual ao nome da estrutura, sem exceção.
  • O construtor não retorna qualquer valor, sem exceção

Refazendo o nosso exemplo

 #include <iostream>
 #include <string>
 using namespace std;

 struct Person {
    string name;
    int height;
    Person()
    {
           name = "Nenhum nome assinalado";
           height = -1;
    }
 };
 int main ()
 {
    Person p1; 
    cout << "O nome da pessoa é "<< p1.name << " e a altura é " << p1.height << endl;
    cin.get();
    return 0;
 }

Repare que demos valores padrões ás variáveis. Agora não estamos no caso de ter p1.name=??? Por mais instâncias que criemos eles vão ter sempre valores padrões.

Construtor com argumentos

Termos um construtor sem argumentos é um melhoramento face ao construtor defaut pois agora temos valores padrões para as variáveis membro. Porém seria melhor se conseguíssemos inicializar as variáveis membro com valores dados pelo utilizador enquanto o programa estivesse sendo executado. E realmente podemos fazer se passarmos argumentos.

 #include <iostream>
 #include <string>
 using namespace std;
 
 struct Person 
 {
    string name;
    float height;
    Person()	//construtor sem argumentos
       {
       name = "Nenhum nome assinalado";
       height = -1;
       }
    Person(string s, float h)  //construtor com 2 argumentos
    {
       name = s;
       height = h;
    }
 };  
 
 int main ()
 {
   float metro;
   string strName;
   cout << "Entre com o nome da pessoa: ";
   getline(cin, strName);
   cout << "Entre com a altura em metros: ";
   cin >> metro;
   cin.ignore();   
   Person p1(strName,metro); 
   cout << "O nome da pessoa é " << p1.name << " e a altura é " << p1.height << endl;
   cin.get();
   return 0;
 }


Repare que os argumentos do construtor têm de estar na ordem esperada

Separar o construtor prototype da implementação
 #include <iostream>
 #include <string>
 using namespace std;
 
 struct Person {
   string name;
   float height;
   
   Person();		//construtor sem argumento
   Person(string, float);	//construtor com dois parâmetros, apenas é necessário dizer o tipo dos parâmetros – o nome não é necessário)
 };
 
  Person::Person()
  {
      name = "Nenhum nome assinalado";
      height = -1;
  }
  
  Person::Person(string s, float h)
  {
      name = s;
      height = h;
 }
 
 int main ()
 {
   float metro;
   string strName;
   cout << "Entre com o nome da pessoa: ";
   getline(cin, strName);
   cout << "Entre com a altura da pessoa em metros: ";
   cin >> metro;
   cin.ignore();   
   Person p1(strName, metro); 
   cout << "O nome da pessoa é " << p1.name << " e a altura é " << p1.height << endl;
   cin.get();
   return 0;
}
  • Vamos ver a função main(): declaramos 2 variáveis uma float e outra string. Pedimos para a pessoa escrever o nome e colocamos o valor na variável string, depois pedimos a altura e colocámos na variável float. Depois chamamos o construtor com dois argumentos e passamos as variáveis anteriores como argumentos. Por fim mandamos imprimir os valores das variáveis membro da estrutura.
  • Repare que para definirmos fora o construtor recorremos ao operador scope ::
 Person::Person()
 Person::Person(string s, int h)
  • Repare que no prototype dos construtor apenas tivemos de dizer o tipo dos parâmetros


Os operadores realizam, como o nome sugere, operações entre dois tipos de dados. Existem muitos operadores, mas citaremos aqui os fundamentais e, conforme as necessidades dos tópicos posteriores, citaremos todos os demais.

Compatibilidade

editar

Os operadores padrões da linguagem "C" podem ser usados de forma semelhante na linguagem C++. Aliadas às funcionalidades tradicionais do "C" podemos criar operações diferentes para os operadores. Esta funcionalidade é conhecida como sobrecarga de operadores e será descrita em capítulo posterior.

Como C++ interpreta os operadores

editar

Para dar uma base de entendimento para a sobrecarga de operadores iremos introduzir o modo como o compilador C++ entende os operadores. Este modo de interpretar os operadores é implementado para dar suporte aos recursos de POO da linguagem. O entendimento do modo de funcionamento do mecanismo de compilação pode diminuir as dúvidas que surgem no estudo de operadores em C++.

É importante entender que a linguagem deve servir de base para a criação de entidades de dados autônomas e operáveis. Logo, o compilador deve ser generalista ao enxergar operações com tipos primitivos e tipos criados pelo programador (classes). Por estes fatores, o compilador utiliza-se de uma interface funcional para implementação dos recursos de operadores.

Os operadores são tratados como funções, de forma a possibilitar a alteração do seu comportamento em determinados casos. Os operandos são tratados como argumentos de funções, enquanto que o resultado da operação é tratado como retorno da função do operador. Esta interface faz com que possamos programar o comportamento das operações e alterá-las quando nos seja conveniente.

Entendendo o operador

editar

Basicamente, temos dois tipos de implementação para operadores, o tipo global e o tipo membro de classe. Os dois tipos são usados regularmente nas implementações mais comuns. Analisaremos o tipo global, uma vez que ainda não temos uma noção de classes suficiente para abordar o tipo membro de classe.

Digamos que temos uma estrutura ponto, como definida abaixo:

 struct Ponto
  { int x;
    int y;
  };

Uma vez que tenhamos definido um ponto, nada mais natural que queiramos somar, subtrair, enfim operar, pontos diferentes de acordo com nossas necessidades. Para isso podemos criar operadores para fazer isso, da seguinte forma:

  • Observamos a quantidade de parâmetros, o retorno e a forma de chamar o operador que queiramos definir e criamos uma função que execute a operação desejada;
  • Inserimos o código da referida função dentro de uma chamada de operador, usando a palavra reservada operator seguida do operador que desejamos definir:
  Ponto operator+ ( Ponto a, Ponto b )
        { Ponto soma; 
          soma.x = a.x + b.x;
          soma.y = a.y + b.y;
          return soma;
        }

E assim, o operador é entendido como uma função, sendo a sobrecarga um processo de definição da operação a ser executada. Recebe esse nome porque todo operador já existe e a definição de uma nova funcionalidade apenas adiciona (sobrecarrega) as habilidades anteriores do operador. Embora isto seja comum, é bom lembrar que operações de tipos primitivos não poderão ser modificadas, restando apenas a funcionalidade de criar operadores para nossos tipos (classes).

Os argumentos

editar

Agora vejamos como os argumentos são vistos pelo compilador durante a chamada ao operador. Essa sintaxe, muitas vezes, confunde iniciantes, mas é bastante intuitiva. Veja:

c = a + b;

Consideremos que a,b,c são pontos. Em termos gerais, qualquer operador binário (com dois argumentos) definido em escopo global, receberá a e b como primeiro e segundo argumento da função que define o operador.

Podemos ver a chamada da seguinte forma:

 c = operator+( a, b );

No momento da operação, o compilador montará a chamada do operador "+" da forma que escrevemos acima, ou seja como uma função que recebe os pontos "a" e "b" como argumentos e retorna o resultado, como operado na declaração anterior, no ponto "c". De fato, "c" será ( a.x + b.x, a.y + b.y ) como foi operado na declaração da função ( operator+ ) discutida no tópico anterior.

Operadores aritméticos

editar

Operadores aritméticos são utilizados para efetuar operações matemáticas entre dados. São 5 operadores aritméticos em C++:

#include <iostream>
using namespace std;

int main() {

int soma = 5 + 5; // o operador '+' realiza somas.
double subtracao = 5 - 5; // o operador '-' efetua subtração.
float multiplicacao = 5.1 * 0.5; // o operador '*' efetua multiplicação.
char divisao = 100 / 2; // o operador '/' efetua divisão.
int modulo = 51 % 5; // retorna o resto da divisão inteira.

cout << "Resultados: " << soma << ", " << subtracao << ", " << multiplicacao << ", "
     << divisao << ", " << modulo << endl;

}

A saída desse programa gera no console o seguinte:

Resultados: 10, 0, 2.55, 2, 1.

O quarto resultado é '2' pois 50 é o código decimal deste caracter.

Tipo de retorno

editar

Você pode realizar operações aritméticas, obviamente, entre números. Como dito no tópico anterior, você também pode realizar operações aritméticas com os tipos char e wchar_t.

O retorno da operação será também um número (real, inteiro ou até mesmo um caracter, conforme os tipos dos operandos).

Divisão inteira e divisão real

editar

Existe, para a linguagem, diferença entre uma divisão entre números inteiros e entre números reais (ponto flutuante). Se você fizer a divisão entre os inteiros 3 e 2, o resultado não será 1.5, será 1. Já se fizer a divisão entre os números reais (em ponto flutuante) dos deles, então sim obterá 1.5.

O motivo é que há 2 tipos de divisão: a inteira e a decimal.

Divisão inteira e o operador módulo

editar

A divisão inteira retorna o quociente da divisão sem a parte fracionária. Isso ocorre porque a linguagem efetua a divisão enquanto o resto for maior que o divisor (logo, a divisão nunca apresentará parte fracionária).

Para obter o resto da divisão, você pode usar o operador módulo (%). Esse operador retorna, em vez do quociente, o resto da divisão inteira. É por isso que no nosso exemplo 51 % 5 resultou em 1, pois 5x10 + 1 = 51, onde 5 é o divisor, 10 é o quociente, 1 é o resto e 51 o dividendo.

Divisão real

editar

A divisão real é aquela efetuada entre tipos ponto flutuante ou entre ponto flutuante e inteiros/caracteres. Isso efetuará a divisão até que o resto seja zero, ou quando o resto repetir-se indefinidamente (no caso de dízimas periódicas como, por exemplo, 10/3).

Se quisermos que a divisão entre inteiros retorne a divisão real, deveremos efetuar uma conversão explícita, conforme o exemplo:

int num = 3;
int num2 = 2;

cout << "Resultado: " << (float) num/num2 << endl;
// o resultado foi convertido para ponto flutuante explicitamente.

Aritméticos com atribuição

editar

Temos, inspirados pelas operações de processador, operadores que efetuam as operações e em seguida atribuem o valor das operações a uma das variáveis participantes do processo. Temos os operadores "+=", "-=", "*=", "/=" e "%=" que basicamente funcionam da seguinte maneira:

int a,b;

a = 2; 
b = 3;
a += 3;

cout << a << endl; // Mostrará o valor da soma de "a" e "b" antes da operação, neste caso: "5";

a = 8;
b = 5;
a -= b;

cout << a << endl; // Mostrará o valor da subtração de "a" e "b" antes da operação, neste caso: "3";

a = 4;
b = 7;
a *= b;

cout << a << endl; // Mostrará o valor da multiplicação de "a" e "b" antes da operação, neste caso: "28";

a = 30;
b = 3;
a /= b;

cout << a << endl; // Mostrará o valor da divisão de "a" e "b" antes da operação, neste caso: "10";

a = 28;
b = 5;
a %= b;

cout << a << endl; // Mostrará o resto da divisão de "a" e "b" antes da operação, neste caso: "3";

Observemos o que há em comum nas operações acima, em todos os exemplos temos duas variáveis "a" e "b" e um operador aritmético com atribuição. O que efetua-se em todos os exemplos é uma operação aritmética com uma atribuição final do valor calculado a uma das variáveis, que neste caso é o "a". A sintaxe é simples de entender, sempre o valor de "a" é operado com o valor de "b" e depois o valor do resultado é depositado em "a".

Operadores lógicos bit a bit

editar

Existem operadores que nos permitem fazer operações lógicas, os mais básicos são os operadores "&" , "|" , "^" e "~", que são respectivamente correspondentes às operações lógicas "E", "OU", "OU exclusivo" e "Não bit a bit". Vejamos como podemos operar números usando-os:

Se tivermos que operar os números hexadecimais: "1A2C" e "2B34" podemos fazê-lo da seguinte forma:

int Va = 0x1A2C;
int Vb = 0x2B34;

int Vc = Va & Vb; // temos Va = 0001 1010 0010 1100 e Vb = 0010 1011 0011 0100
                  // Façamos Va = 0001 1010 0010 1100
                  //         Vb = 0010 1011 0011 0100
                  //    Va & Vb = 0000 1010 0010 0100 logo 0A24

cout << hex << Vc << endl;  // Isto mostrará na saída: "a24" que corresponde a operação "E" dos dois números.

Vc = Va | Vb;     // Executaremos Va "OU" Vb
                  //    Va | Vb = 0011 1011 0011 1100 logo 3B3C

cout << hex << Vc << endl;  // Isto mostrará na saída: "3b3c" que corresponde a operação "OU" dos dois números.

Vc = Va ^ Vb;     // Executaremos Va "OU exclusivo" Vb
                  //    Va ^ Vb = 0011 0001 0001 1000 logo 3118

cout << hex << Vc << endl;  // Isto mostrará na saída: "3118" que corresponde a operação "OU exclusivo" dos dois números.

Vc = ~Va;     // Executaremos "Não bit a bit" ou "complemento" de Va
                  //    Va ^ Vb = 1110 0101 1101 0011 logo E5D3

cout << hex << (Vc & 0xFFFF) << endl;  // Isto mostrará na saída: "e5d3" que corresponde a operação "complemento" de Va.

Observe que convertemos, nos comentários, os números para binário a fim de facilitar a visualização de cada operação feita bit a bit. De forma simples o resultado é obtido e convertemos na saída o número para o formato hexadecimal, apenas para facilitar a visualização.

Devemos observar o detalhe da última operação com mais critério: Vejam que quando fomos mostrar o resultado da mesma tivemos que efetuar a operação (Vc & 0xFFFF). Isto se deve ao fato de que ao operar o número armado em "Va" o compilador poderá ver o número 1A2C como 00001A2C, ou 0000000000001A2C, ou outros valores, dependendo do tamanho do tipo int gerado pela máquina a qual se destina a compilação. Neste caso o resultado da operação seria acrescida de FFFF no início do número do resultado. Portanto se fizermos a operação (Vc & 0xFFFF) eliminamos os números FFFF ou FFFFFFFFFFFF do início do número resultante.

A estes operadores lógicos bit a bit podemos ainda adicionar os operadores compostos da linguagem "C": "&=", "|=" e "^=" que são, respectivamente: "E" com atribuição, "Ou" com atribuição e "Ou exclusivo" com atribuição. A operação dos três operadores tem em comum o fato de funcionarem de forma semelhante. Primeiro se faz a operação "E", "Ou" ou "Ou exclusivo" entre a variável à esquerda e o número ou variável à direita, depois se atribui o resultado da operação à variável da esquerda. Vejamos os exemplos abaixo:

int Va = 0x1A2C;
int Vb = 0x2B34;

Va &= Vb;
cout << hex << Va << endl;  // Isto mostrará na saída: "a24" que corresponde a operação "E" dos dois números.

Va = 0x1A2C;
Va |= Vb;
cout << hex << Va << endl;  // Isto mostrará na saída: "3b3c" que corresponde a operação "OU" dos dois números.

Va = 0x1A2C;
Va ^= Vb;
cout << hex << Va << endl;  // Isto mostrará na saída: "3118" que corresponde a operação "OU exclusivo" dos dois números.

Notemos que os resultados são os mesmos que obtivemos quando operamos com os operadores sem atribuição, mostrados anteriormente. A diferença no modo de operação está no fato de que, ao operarmos duas variáveis, a primeira sempre recebe o resultado da operação. Este comportamento, na verdade, imita o modo de operação de alguns registradores dentro dos microprocessadores. Certamente o compilador aproveitará esta capacidade, dependendo do processador, para otimizar o código de máquina gerado durante o processo de compilação.

Operadores comparativos

editar

Para efetuarmos comparações dentro do código dos programas precisamos de operadores que as efetuem, nas linguagens "C" e "C++" temos os operadores: "<" (menor que), ">" (maior que), "==" (igual a), "<=" (menor ou igual a), ">=" (maior ou igual a), "!=" (diferente de) que retornam o valor booleano, ou seja verdadeiro ou falso. Além destes comparativos podemos efetuar operações lógicas entre duas variáveis ou valores com os operadores lógicos comparativos "&&" (E), "||" (Ou), que retornam valor booleano, e podemos comparar com o complemento lógico de um valor ou variável usando o "!" (Não).

Todos estes operadores funcionam em conjunto com as operações condicionais, como o "if" por exemplo. Vejamos alguns exemplos:

#include <iostream>

using namespace std;

int main()
{
 int a,b;

 cout << "Digite um valor para a: ";
 cin >> a;

 cout << "Digite um valor para b: ";
 cin >> b;

 if ( a < b ) cout << "a é menor que b." << endl;

 if ( a > b ) cout << "a é maior que b." << endl;

 if ( a == b ) cout << "a é igual a b." << endl;

 if ( a <= b ) cout << "a é menor ou igual a b." << endl;

 if ( a >= b ) cout << "a é maior ou igual a b." << endl;

 if ( a != b ) cout << "a é diferente de b." << endl;

 return 0;
}

Neste simples programa, usamos os operadores comparativos básicos. Nele, o usuário poderá digitar dois números e depois o programa informará os comparativos de magnitude dos dois números, embora os faça de maneira bastante simples, o usuário terá condições de saber se os números são iguais ou diferentes e se um é maior ou menor que o outro.

Se quisermos aninhar condições, criando decisões mais complexas, podemos usar os operadores "&&", "||" e "!". Com esses operadores podemos combinar duas ou mais condições, por exemplo: Se quisermos estabelecer uma condição onde um número "b" está entre um valor "a" e outro valor "c" podemos fazer:

#include <iostream>

using namespace std;

int main()
{
 int c;

 cout << "Digite um valor entre 5 e 14: ";
 cin >> c;

 if ( (c < 14) && (c > 5) )
   cout << "O valor está dentro do limite esperado." << endl;

 return 0;
}

Neste simples exemplo, se o usuário digitar um número dentro do limite entre 5 e 14 o programa emitirá a mensagem, caso contrário não. Note que precisamos condicionar o número a estar abaixo de 14 "e" acima de 5, assim estabelecemos a faixa onde o programa reconhece como correta. A operação "(c < 14)" retorna verdadeiro quando c é menor que 14 e a operação "(c > 5)" retorna verdadeiro quando c é maior que 5, de fato, se tivermos verdadeiro nas duas operações o operador "&&" retorna verdadeiro, se qualquer das duas operações retornar falso o operador "&&" retorna falso.

Demais operadores

editar

Os demais operadores da linguagem C++ desempenham diversas funções, o que impossibilita agruparmos eles em grupos específicos. Vejamos o que cada um faz e assim teremos como usá-los nos exemplos logo abaixo, o que nos possibilitará ter um breve contato com o uso dos mesmos.

O operador =

editar

O operador "=" é conhecido como operador de atribuição, como sua função em matemática é bem conhecido seu uso é bem intuitivo, apenas atribui um valor ou o conteúdo de uma variável no lado direito a uma variável do seu lado esquerdo. Acreditamos que este não necessita de exemplo, já que foi usado nos exemplos anteriores.

O operador []

editar

O operador "[]" usa-se "a[n]" é conhecido como operador de arranjo ou acesso a elemento de vetor. Este é bem mais específico das linguagens de programação baseadas na linguagem "C" e devemos analisar melhor o seu funcionamento. Digamos que temos que criar um conjunto de valores em forma de arranjo, ou "array", designado pela letra "a", então temos a1, a2, a3, ... an. Neste caso devemos declará-lo da seguinte forma:

 int a[5];

Isto significa que alocamos 5 posições de memória de tipo "int" para ser usadas como arranjo, cujo nome é "a". Veja que a função de "[]" é de alocar a quantidade de 5 elementos para serem usados no arranjo. Porém, este mesmo operador tem outra função, que é de acesso aos valores dentro do arranjo, vejamos o próximo exemplo:

 b = a[3];

Esta linha de código mostra como podemos atribuir à variável "b" o valor contido na quarta posição do vetor que declaramos anteriormente. Observe que apesar de termos colocado a[3] obtemos o quarto valor pois a posição a1 está atribuída ao elemento a[0], assim, se quisermos acessar o elemento a4, devemos acessar o elemento a[3].

O opeador ()

editar

O operador "()" - parênteses também tem duas funções, embora que bem distintas. A primeira e a de agrupar operações matemáticas, como podemos ver no exemplo logo a seguir:

 c = ( a + b ) * 10;

Nesta simples operação agrupamos a operação "a + b" para que seja efetuada antes da operação de multiplicação, trata-se do uso trivial da matemática, funciona da mesma forma: adiciona-se o que está entre parênteses antes de efetuar a multiplicação declarada logo após os mesmos. Os parênteses podem ser combinados de diversas formas, sempre seguindo o uso análogo dos agrupadores matemáticos, a diferença é que não fazemos, como na matemática, o uso dos colchetes "[]" e das chaves "{}" como agrupadores. Para estas funções usamos sempre parênteses, não importando se já existam parênteses dentro de um certo agrupamento. Vejamos outro exemplo:

 e = ((( a + b ) * 10 - 1)*c - d)*8 - 3;

Em matemática faríamos:

 

Observe que, na linguagem "C" ou "C++", podemos sempre adicionar um parêntese para agrupar, não precisamos passar de parêntese para colchetes e depois para chaves, na verdade não podemos fazer isso, pois estes são operadores com outras funções nas linguagens de programação.

A outra função do operador "()" é a chamada e declaração de função, além da participação em outros elementos de linguagem como laços, operações lógicas e switch. Vejamos alguns exemplos:

void myswitch(int a)
{
 while (a < 4)
 { switch(a)
   { 
     case 0: load();
        break;
     case 1: configure();
        break;
     case 2: execute();
        break;
     case 3: unload();
        break;   
   }
   process_next();
 }
}

Observemos que o operador "()" é bastante usado nas linguagens "C", "C++" e derivadas... Destacamos neste exemplo a declaração da função "myswitch", o uso no laço "while" e no comando "switch", além do uso na chamada de funções "load", "configure", "execute" e "unload". Seu uso é bastante extenso e poderá ser bem explorado no decorrer da leitura deste livro.

O operador () ao preceder uma variável ou valor pode agir como agente de "typecasting", ou seja, forçar que o compilador entenda que o tipo de dado é o apresentado entre parênteses. Vejamos o exemplo:

 void func( int x );
 ...
 float a;
 ...
 func( (int)a );

Neste trecho de código vemos que a função espera uma variável inteira como argumento de entrada, porém o programador quer passar uma variável tipo "float" ( que em uma máquina arbitrária qualquer tem o mesmo tamanho de um inteiro) para a função e para isto ele usa o "typecasting" na chamada da função, com isto o compilador ignora a verificação de tipo na entrada da função e aceita a variável como se ela fosse inteira.

Outro uso interessante de uso do operador "()" é a conversão de tipo, faz-se "tipo(a)" para transformar uma variável de um tipo específico para outro. Observe que isto é um pouco diferente de fazer (tipo)a que não transforma nenhum tipo em outro, mas sim fazer como no exemplo abaixo:

 void func( char y );
 ...
 int a = 'v';
 char x;
 x = char(a);
 ...
 func(char(a) );

Neste caso a variável "a" será convertida em um inteiro. Imaginem uma máquina em que o tipo "char" tem 8 bits e tipo "int" tem 32 bits, então o valor inteiro será transformado de 32 bits para 8 bits. Isto se torna mais relevante na chamada da função, quando só existem 8 bits na entrada da mesma, e por isso, a chamada de "char(a)" transforma o inteiro em um caractere.

O opeador *a

editar

Como já vimos anteriormente o operador "*" é um dos operadores aritméticos, o da multiplicação, mas podemos usá-lo com outra função, como declarador de ponteiro e acesso a valor apontado por ponteiro. Primeiramente se declaramos algo como:

 int *a;

Neste caso estamos declarando um ponteiro para um valor inteiro armazenado na memória. Significa dizer que se quisermos colocar um endereço de memória na variável "a" poderemos fazê-lo, desde que o endereço pertença a um valor inteiro, assim, poderemos manipulá-lo quando quisermos. Isso nos indica a necessidade de uma segunda função ao operador "*" quando usado com ponteiros, a de apontar para o valor armazenado na memória cujo endereço está em "a", vejamos:

 int *a = 0x3B12242C58E50023;
 *a = 5 + 8;
 cout << *a;

Imaginemos que, numa máquina hipotética, tenhamos a sequência de código acima, então no endereço 3B12242C58E50023, será armazenado o valor 13, resultado da operação acima, logo após será mostrado este valor na saída do dispositivo. Porém, devemos nos atentar a uma particularidade das linguagens baseadas em "C"... na primeira linha, quando fizemos: "int *a = 0x3B12242C58E50023;" estamos declarando a variável ponteiro "a" e, só neste caso, o número que colocamos se refere ao endereço de memória para o qual ela aponta, nos demais casos que se seguem, quando fazemos "*a = " estamos colocando valores dentro da memória apontada e quando fazemos "*a" estamos nos referindo ao valor armazenado na memória apontada e não em "a". Parece um pouco confuso, não é? Mas tudo isso será abordado em mais detalhes na seção onde se aborda ponteiros e mais ainda no livro "Programar em C". Na verdade, para melhor entendimento, recomendamos que o leitor inicie o estudo de ponteiros pelo livro da linguagem "C", certamente o entendimento será muito melhor.

O opeador &a

editar

Podemos chamar este operador de "endereço de" ou "referência a", quando o operamos junto com ponteiro podemos extrair o endereço de uma variável e, assim, colocá-lo no ponteiro. Sempre que quiseremos atribuir a um ponteiro um endereço de uma variável podemos usar o operador "&" como vemos no exemplo abaixo:

 int x = 23; 
 ...
 int *a;
 ...
 a = &x;
 cout << "O valor apontado pelo ponteiro a é " << *a <<endl;

Como resultado deste trecho de código teremos na saída padrão: "O valor apontado pelo ponteiro a é 23". Note que quando fazemos "a = &x" estamos colocando o endereço da variável "x" no ponteiro "a", assim, quando fazemos "*a" acessamos o valor armazenado pela variável, que neste caso é "23".

Temos outro uso do operador "&" na linguagem C++ que é dar referência de uma variável a outra. Em termos gerais funciona como dar um apelídio a um símbolo de forma que este passe a operar o conteúdo do outro. Vejamos um exemplo:

 int a;
 int &b = a;

 a = 2;
 cout << "O valor armazenado em a é: " << b << endl;

Assim, o valor armazenado em "a" será o mesmo ao qual nos referimos quando acessamos "b", podemos usar tanto "a" como "b" para operar valores na variável em qualquer parte do programa pois eles se referem a uma só variável com dois nomes diferentes.

Quando declaramos uma função e usamos, na declaração dos argumentos, o operador "&" passamos a operar os argumentos por chamada de referência, ou seja, se alterarmos o valor do argumento dentro do escopo da função também alteraremos o valor da variável que foi passada na chamada da função, ou seja, escopo acima. Vejamos um exemplo:

 void func(int &x)
     {
       ...
       x = 8;
       ...
     }

 int a = 3;
 func(a);
 cout << "O valor da variável é: "<< a << endl;

Observe que o valor mostrado na saída padrão será "8" pois a função "func" recebe a variável "x" como referência de uma variável externa ao seu corpo, desta forma, quando se chama a função "func(a)" temos "a" e "x" como nomes de uma mesma variável e dentro da função, esta variável é alterada para o valor "8".

O opeador ::

editar

Este operador é característico da linguagem C++ e apresenta uma função pertencente à orientação a objeto, refere-se a característica de declarar e se referir a elementos de outros escopos que não pertencem ao escopo atual. Por exemplo, quando queremos criar uma classe e não desejamos colocar o código das funções membro dentro da declaração da mesma utilizamos o operador de resolução de escopo, veja o código a seguir:

class top{

       int a, b;
       public:
              int c;
              void print();
              int get_a();
              int get_b();
};

void top::print()
{  cout << "Valores armazenados: " << a <<"; "<< b <<"; "<< c << "; " << endl;
}

int top::get_a()
{  return a;
}

int top::get_b()
{  return b;
}

Como podemos ver as funções membro "print()", "get_a()" e "get_b()" foram declaradas fora do escopo da classe, e para isso temos que adicionar o nome da classe seguido do operador "::" imediatamente antes do nome da função, esta sintaxe faz com que indiquemos ao compilador que a função que está sendo escrita pertence a classe "top", que propositalmente foi denominada desta forma para melhor entendimento. A classe topo, abriga os elementos: "top::a", "top::b", "top::c", "top::print()", "top::get_a()", "top::get_b()" e assim eles podem ser referenciados fora do corpo da mesma desde que obedecendo as regras da linguagem.

Controle de fluxo em C++

editar

Aqui vamos criar funções que nos permitam alterar a leitura de execução do código. Ou seja, já dissemos que a leitura de execução do código é de cima para baixo (lê a primeira linha de código – executa, lê a 2ª linha --- executa,..e por aí adiante)

O que agora vamos fazer e criar mecanismos para alterar esse leitura sequencial e permitir:

  • Execução de código de acordo com a condição
  • Repetição de execução de código sujeito a condição
  • Saltar linhas de código

Decisão em C++

editar

Em C++ os métodos de tomada de decisão presentes na linguagem C estão disponíveis para as tarefas mais corriqueiras que o programa deve executar. Além desta forma de controle de decisões, C++ provê certas funcionalidades relacionadas a objetos que modificam a forma como o código é estruturado e, por consequência, decidem como o programa deve se comportar em determinadas situações. Examinemos os métodos básicos e analisemos de forma simples as estruturas de decisão presentes no modelo de programação orientado a objetos, para entender como isso poderá nos ajudar a tornar o código mais bem construído.

De modo geral, a maioria das linguagens de programação existentes utiliza-se das estruturas if-else ou switch-case.

if-else

editar

Se você quer que o software execute um determinado comando somente em certas situações, utilize if para determinar isto. O programa vai, então, executar a primeira linha de código após o if, se a declaração entre parênteses for verdadeira. Exemplo:

#include <iostream>
using namespace std;
int main(void) {
    int variavel;
    cout << "Escreva um numero: ";
    cin >> variavel;
    if(variavel == 5)
        cout << "A variável é igual a 5";
    return 0;
}

Pode-se usar valores booleanos:

bool variavel;
if(variavel) //if será executado se booleano for verdadeiro, como não lhe foi atribuído valor, é falso
    cout << "variável é verdadeira!";

Ou, se booleano tiver que ser falso para ocorrer a execução:

if(!variavel) // O ! faz com que só haja execução da próxima linha se variável for falsa
    cout << "variável é falsa!";

Mas se você quiser que o computador execute várias linhas após o if se este for verdadeiro? Basta usar chaves:

if(variavel) {
    cout << "A variável é verdadeira...\n";
    cout << "E continua executando" <<
    "até que seja fechado o if" <<
    " com o }";
}

É também possível usar o bloco else para o computador executar várias linhas de código caso uma condição tenha o valor booleano falso. Por exemplo:

if(temperatura < 20) {
    cout << "Está frio";
} else {
    cout << "Está calor";
}

Assumindo que a variável temperatura tem tipo inteiro (int), se esta contiver um valor, por exemplo, 20, será apresentada na tela a mensagem "Está calor", uma vez que 20 < 20 é uma expressão é contradição, logo tem o valor booleano falso.

Podemos ainda encadear vários else, e obter ainda mais possibilidades:

    if(deposito < 20) {
    	cout << "Depósito de gasóleo inferior a 20%";
    } else if(deposito < 50) {
    	cout << "Tem menos de metade do depósito";
    } else {
    	cout << "Ainda tem meio depósito ou mais";
    }

Desta forma, conseguimos distinguir o nível de depósito de gasóleo de um carro em 3 níveis diferentes. Para distinguir por mais níveis, bastaria acrescentar mais } else if(...condição...) { para distinguir os diferentes patamares.

De notar que, cada caso é avaliado individualmente, por ordem. Isto é, primeiro seria verificado se o depósito tem menos de 20% de gasóleo; apenas se esta condição for falsa é que o computador avalia a segunda condição, para verificar se o depósito tem menos de 50% de gasóleo, e por ainda adiante.

switch

editar

O switch é muito parecido com o if-else. Apenas a sintaxe e construção é diferente

 #include <iostream>
 using namespace std;
 int main(void)
 {
   char grade;
   cout << "Enter your grade (A to F): ";
   cin >> grade;
   switch (grade)
   {
   case 'A':
      		cout << "Your average must be between 90 - 100"<< endl;
      		break; 
   case 'B':
      		cout << "Your average must be between 80 - 89"<< endl;
      		break;
   case 'C':
      		cout << "Your average must be between 70 - 79"<< endl;
      		break;
   case 'D':
      		cout << "Your average must be between 60 - 69"<< endl;
     		break;
   default: 
      		cout << "Your average must be below 60" << endl;
   }
   return 0;
 }
  • Cada um dos casos tem de ser uma constante (não pode alterar durante a vida do programa), neste exemplo A, B, …
  • O defaut serve para a condição de todas as avaliações dos casos anteriores der falsa. (é tipo o else)
  • O break serve para terminar o switch, caso contrário se num dado case fosse verdadeiro, iria executar todos os statementes mesmo de outros cases até terminar o switch.
  • Aqui para cada caso não necessitamos de {} se tivermos mais do que 2 statements.
  • o if-else é mais forte do que o switch por que permite fazer coisas como:
if (apples == oranges)
  do this;
else if (sales >= 5000)
  do that;
  • Para além do ponto já dito de os casos serem obrigatoriamente constantes, no switch
  • Também posso utilizar operadores lógicos no switch
 switch (age >= 18 && citizen == true)
   {
   case true:
      cout << "You are eligible to vote";
      break;
   case false:
      cout << "You are not eligible to vote";
    }

Operador condicional "?"

editar

A sintaxe é :

  • [Relational expression] ? [statement if true] : [statement if false]

Ou seja, este operador testa a expressão relacional e se o resultado for verdadeiro executa logo a 1ª afirmação caso contrário executa a segunda.

ou seja isto não é mais do que um if-else.

Há quem goste de usar este operador porque poupa escrita, mas acho que não vale a pena!

 #include <iostream>
 using namespace std;
 int main(void)
 {
   int num;
   cout << "Enter a whole number: ";
   cin >> num;
   cout << "The number is " << (num % 2 == 0 ? "even" : "odd") << endl;
   return 0;
 }

Notar que o operador condicional exige 3 operandos.

vamos fazer uns exercícios:

  • 7==5 ? 4 : 3 // returns 3, since 7 is not equal to 5.
  • 7==5+2 ? 4 : 3 // returns 4, since 7 is equal to 5+2.
  • 5>3 ? a : b // returns the value of a, since 5 is greater than 3.
  • a>b ? a : b // returns whichever is greater, a or b.

O Comando goto

editar

O goto realiza um salto para um local especificado. Este local é determinado por um rótulo. Portanto pode ser em qualquer parte do programa.

nome_do_rótulo: 
.... 
goto nome_do_rótulo; 
.... 
 // goto loop example
 #include <iostream>
 using namespace std;
 int main ()
 {
  int n=10;
 loop:
  cout << n << ", ";
  n--;
  if (n>0) 
        goto loop;
  cout << "FIRE!";
  return 0;
 }

Repare no rótulo.

Nota: Evite o uso de goto quando for possível usar estruturas principais, porque pode tornar o código ilegível. Esse comando deve ser usado em último caso, como sair de loops aninhados.

Terminando o programa

editar

Esta função é definida com a biblioteca cstdlib (c+std+lib) O propósito da função é terminar com o programa com um específico código de saída O protótipo é:

  • int exit (int exitcode);

Esta função é usada por alguns sistemas operativos e podem ser usadas para chamar programas. Por convenção o código 0 se saída significa que o programa terminou normalmente, como previsto, se vier com outro número significa que houve um erro e algo de inesperado sucedeu.

Laços (loops em inglês), ou estruturas de repetição, são comandos existentes nas linguagens de programação destinados a executar uma ou mais instruções quantas vezes forem necessárias. Cada ciclo de um loop é chamado de iteração. Podemos ter também loops dentro de outro loop.

O while, "enquanto" em inglês, é um laço que ordena o computador a executar determinadas instruções enquanto uma condição for verdadeira. Isso faz com que um comando seja executado uma vez a cada verificação da condição. De modo geral o comando sempre deve ser elaborado de forma que se leve a condição de execução a ser falsa em algum momento, de forma a interromper o laço para que o resto do programa entre em execução.

Sintaxe

editar
while (condição)
  comando;

Onde condição é a condição de execução do laço while.

O código abaixo mostra o uso do laço while para imprimir na tela do número 1 ao número 10. Perceba o uso de uma variável inteira intitulada contador. Esta variável é utilizada para armazenar um valor a ser impresso bem como participar da condição de execução do laço. Assim que a variável atingir o valor 11 o programa segue para o comando logo após o laço.

#include <iostream>

using namespace std;

int main()
{
  int contador;               // Declara a variável contador.
  contador=1;                 // contador recebe o valor 1.
  while (contador<=10)        // Enquanto contador for menor ou igual a 10.
  {
    cout << contador << endl; // Imprime contador.
    contador++;               // Incrementa contador em uma unidade.
  }
  return 0;
}

Do-While

editar

O laço do-while é um while invertido, onde você coloca as instruções a serem repetidas antes da verificação da condição de execução. Isso significa que os comandos do laço serão executados ao menos uma vez.

Sintaxe

editar
do {
  comando;
} while (condição);

Onde condição é a condição de execução do laço do-while. Os comandos pertencentes ao laço somente deixarão de se repetir quando a condição for falsa.

O algoritmo abaixo mostra como seria o algoritmo exemplo usado na seção do laço while convertido para o uso do laço do-while.

#include <iostream>

using namespace std;

int main()
{
  int contador;               // Declara a variável contador.
  contador=1;                 // contador recebe o valor 1.
  do {
    cout << contador << endl; // Imprime contador.
    contador++;               // Incrementa contador em uma unidade.
  } while (contador<=10);     // Enquanto contador for menor ou igual a 10.
  return 0;
}

Como o uso de uma variável como contador nos laços é algo frequente, foi criado um outro laço que traz em sua estrutura campos para abrigar os comandos de atribuição de valor inicial e incremento/decremento do contador. O nome deste laço é for, "para" em inglês.

Sintaxe

editar
for ([inicialização]; [condição]; [incremento])
  comando;

Onde:

  • inicialização: campo destinado para qualquer comando que deve ser executado, uma única vez, logo no início do laço.
  • condição: campo destinado para a condição de parada. Esta condição é verificada logo no início do laço, imediatamente após a conclusão do parâmetro1 e a cada passo quando o <comando> é executado.
  • incremento: campo destinado para qualquer comando que deve ser executado todas as vezes em que o laço finalizar seu último comando, em todas as suas repetições.

O algoritmo abaixo mostra o uso do laço for manipulando a variável inteira contador de maneira a imprimir uma contagem de 1 até 10. Esse uso do laço for é o mais comum, pois possibilita uma repetição de comandos de número fixo, dez no algoritmo em questão.

#include <iostream>

using namespace std;

int main()
{
  int contador;                              // Declara a variável contador.
  for (contador=1; contador<=10; contador++) // Inicia o laço.
    cout << contador << endl;                // Imprime contador.
  return 0;
}

É importante deixar claro que nenhum dos três parâmetros utilizados no laço for é obrigatório. Caso não haja necessidade de utilizar um ou mais deles, basta deixar seu espaço em branco. Como exemplo temos o algoritmo a seguir, que demonstra um laço infinito usando for:

#include <iostream>

using namespace std;

int main()
{
  for (;;)
    cout << "Eu sou um laço infinito." << endl;
  return 0;
}

A ausência do 2º parâmetro significa loop infinito:

 for (int num =1; ; num++)
  {
     cout << num << " ";
  }

Isto vai colocar um valor a mais no num indefinidamente.

Ausência do 3º parâmetro:

 for (int num=1; num <= 10;    )
 {
     cout << num << " ";
     num++;
 }

Bloco de Comandos

editar

Em muitos casos, os laços devem repetir dois ou mais comandos. Para isso, necessitamos criar um bloco de comandos contendo todas as instruções a serem repetidas. A criação de um bloco de comandos é simples, basta colocar todos os comandos entre chaves { }. Os algoritmos de exemplo dos laços while e do-while fazem uso de um bloco de comandos.

Caso o laço não encontre a abertura de um bloco logo em seguida, ele assumirá que o comando imediatamente abaixo é o único que deve ser repetido.

Exemplo 1:

while (condição)
  comando1; // Este comando faz parte do laço.
comando2;   // Este comando não faz parte do laço.

Exemplo 2:

while (condição)
{
  comando1;  // Este comando faz parte do laço.
  comando2;  // Este comando faz parte do laço.
}

O Comando break

editar

O que o break faz é quebrar a execução para fora do bloco de código onde ele está presente:

 #include <iostream>
 using namespace std;
 int main(void)
 {
   int num;
   char choice;
   bool quit = false;
   while (true)
   {
      cout << "Enter a positive number: ";
      cin >> num;
      if (num > 0)
      		break;
      else
      {
      cout << "Number must be positive; try again (Y/N): ";
      cin >> choice;
      if (choice != 'Y')
     	 {
      	quit = true;
      	break;
      	}
      }
   }
   if (quit == false)
      cout << "The number you entered is " << num << " ";
   else
      cout << "You did not enter a positive number";
   return 0;
 }

O break faz com que a execução do programa continue na primeira linha seguinte ao loop ou bloco


  1. include<iostream>
  2. include<stdio.h>

int main() { int a[10], i;

for(i=0;i<10;i++) 
a[i]=i+i; 

} Identifique os valores das iterações abaixo: O valor do elemento x[3] é ___. O valor do elemento x[9] é ___.

Incrementar/decrementar

editar

Aqui vamos voltar a um tópico anterior que foi abordado nos operadores Temos

  • a=a+1 é equivalente a ter a+=1 e ainda a ter a++

Mas isto tudo é só no caso do incremento ser 1.

Podemos ter ++a ou ainda a++. Eles são parecidos mas diferentes, é a questão do prefixo e pós-fixo. A diferença é que

  • O prefixo, faz o incremento ainda durante a instrução
  • O pós-fixo faz o incremento quando se passa para a instrução seguinte.
 #include <iostream>
 using namespace std;
 int main(void)
 {
   int num = 2;
   cout << num << ”\n;
   cout << ++num << ”\n;
   cout << num++ <<”\n;
   cout << num << ”\n;
   return 0;
 }

Portanto

  • int num = 5;
  • cout << (++num == 5);

Exercícios

editar

Crie um programa que dê o fatorial de um número:

 #include <iostream>
 using namespace std;
 int main(void)
 {
   int num, counter, total = 1;
   cout << "Enter a number: ";
   cin >> num;
   cout << "The factorial of " << num << " is ";
   for (int counter = 1; counter <= num; counter++)
      total *= counter;
   cout << total;
   return 0;
 }

Crie um programa para o utilizador adivinhar um número de 0 a 3. Dê 3 hipóteses para adivinhar. No caso de acertar antes de chegar ao fim das 3 hipóteses termine.

 #include <iostream>
 using namespace std;
 int main(void)
 {
   int num, counter, secret = 3;
   cout << "Guess a number between 1 and 10\n";
   cout << "You have 3 tries\n";
   for (int counter = 1; counter <= 3; counter++)
   {
      cout << "Enter the number now: ";
      cin >> num;
      if (num == secret)
      {
         cout << "You guessed the secret number!";
         break;
      }
   }
   cout << "Program over";
   return 0;
 }


Criação de menu.

 #include <iostream>
 using namespace std; 
 int main ()
 {
   int  i;
   do
    {
       	cout << "\n\nEscolha a fruta pelo numero:\n\n";
       	cout << "\t(1)...Mamao\n";
       	cout << "\t(2)...Abacaxi\n";
       	cout << "\t(3)...Laranja\n\n";
       	cin >> i; 
    } while ((i<1)||(i>3));
   switch (i)
       {
       	case 1:
               	cout << ("\t\tVoce escolheu Mamao.\n");
       	      break;
       	case 2:
               	cout <<"\t\tVoce escolheu Abacaxi.\n";
       		break;
       	case 3:
               	cout << ("\t\tVoce escolheu Laranja.\n");
       		break;
        }
   return(0);
 }

Função, do latim functio, onis, representa na computação, um pequeno algoritmo com uma função simples e bem definida. É como se cada função fosse um micro programa, ou um tijolo na construção do programa principal. O uso de funções facilita muito o desenvolvimento, pois, divide o problema principal em pequenos problemas mais simples. Essa técnica se chama, Dividir para conquistar.

A experiência mostra que o uso de funções facilita e acelera a criação e manutenção de sistemas.

Todo programa em C++ tem pelo menos uma função, o main. Veja o exemplo do programa em C++:

#include <iostream> //Biblioteca com funções de entrada e saída de dados

using namespace std; 

int main (void) //Função principal do programa
{
  cout << "Olá mundo!"; //cout também é uma função, e precisa ser importada da biblioteca iostream
  
 //Esta função main retorna um valor int, ou inteiro, por isso faz a operação de retornar 0
 return 0;
}

Do exemplo Olá mundo, vemos que toda função em C++ tem um nome. O nome de uma função junto com o tipo de dados que retorna é chamado assinatura da função.

 int main (void) //Assinatura da função main


Essa assinatura informa que a função de nome main retorna na sua execução um valor do tipo int, ou inteiro, e recebe void como parâmetro. Receber void significa que a função não recebe parâmetro, se a função retorna void, significa que não retorna nada. Algumas funções não precisam retornar nenhum valor para funcionar, apenas realizar alguma ação.

Sobrecarga de funções

editar

Em C++ duas funções podem ter o mesmo nome se:

  • Tiverem um nº diferente de parâmetros e/ou
  • Se os parâmetros forem de tipos diferentes (ints floats,..)

A função não pode ser overloaded apenas com diferentes tipo de retorno de função (ie, uma função retornar ints e a outra retornar floats) então os parâmetros é que interessam.

#include <iostream>
using namespace std;
void ConvertFToC(double f, double &c);
void ConvertFToC(float f, float &c);
void ConvertFToC(int f, int &c);
int main()
{
   double df, dc;
   float ff, fc;
   int i_f,i_c;    //if is a reserved word
   df = 75.0;
   ff = 75.0;
   i_f = 75;
   // The compiler resolves the correct
   // version of ConvertFToC based on 
   // the arguments in each call
   cout << "Calling ""double"" version" << endl;
   ConvertFToC(df,dc);
   cout << df << " == " << dc << endl << endl;
   cout << "Calling ""float"" version" << endl;
   ConvertFToC(ff,fc);
   cout << ff << " == " << fc << endl << endl;
   cout << "Calling ""int"" version" << endl;
   ConvertFToC(i_f,i_c);
   cout << i_f << " == " << i_c << endl << endl;
   system ("pause");
}
void ConvertFToC(double f, double &c)
{
   cout << "In ""double"" version" << endl;
   c = (f - 32.0) * 5. / 9.;
}
void ConvertFToC(float f, float &c)
{
   cout << "In ""float"" version" << endl;
   c = (f - 32.0) * 5. / 9.;
}
void ConvertFToC(int f, int &c)
{
   cout << "In ""int"" version" << endl;
   c = (f - 32) * 5. / 9.;
}

O que é que acontece se tivermos um nº diferente de argumentos entre a chamada e a definição?

A solução aqui proposta é quando não sabemos a quantidade de parâmetros que a função vai ser chamada ou mesmo a tipologia desses argumentos, o que se sugere é fazer várias definições para a função e dar a todas elas o mesmos nome, que o compilador vai saber escolher a definição correcta através do nº e tipologia de argumentos.

Entretanto, por boa prática, as funções não devem ser sobrecarregadas se fizerem operações distintas.

Parâmetros default (padrão)

editar

Pode acontecer que tenhamos que declara varia vezes o mesmo valor como parâmetro de uma função. Para simplificar a chamada a funções que variam pouco podemos definir uma parâmetro default.

#include <stdio.h>
#include <stdlib.h>
/*-----------------------------Cabeçalho--------------------------------*/
/*Definimos uma funçao*/
void function(int a,int b, int c = 100 )
{
     printf("Meu Primeiro argumento :%d\n",a );
     printf("Meu Segundo  argumento :%d\n",b );
     printf("Meu terceiro argumento :%d\n",c );
     getchar();
}
int main (void)
{
    function( 10, 30);
    /* Agora use a função assim e veja o que acontece */
    // function( 10,30,999);        
}

Os parâmetros por default devem ser os últimos da lista, ou seja, mais à direita. O parâmetro padrão deve ser especificado no protótipo e não na declaração da função.

Variáveis de referência

editar

Em C++ podemos criar variáveis que podem ser uma alternativa para os ponteiros em algumas situações. A vantagem de não usar diretamente o endereço (valor de ponteiro) em situações onde não precisamos lidar diretamente com valores de memória torna a programação mais segura e simplificada. Podemos deixar as operações com ponteiros apenas para quando for estritamente necessário.

Variáveis de referência podem ser criadas para dar um nome diferente para as variáveis que já existem no programa, ou para passar a variável para dentro do corpo de uma função. Observemos, inicialmente, um caso simples:

  int  a = 10;
  int &b = a;

  b = 20;

Neste trecho de programa, criamos uma variável de referência b para a variável a, o que significa que criamos outro nome para a variável a. De fato, b é a própria variável a com outro nome, apenas isso. Desta forma, podemos alterar o valor de a usando b.

Passagem de parâmetros

editar

Na linguagem "C", durante a chamada de uma função, os argumentos (parâmetros) têm seus valores copiados para a área de processamento da função. Depois que os mesmos foram usados dentro do bloco de processamento da função, eles são descartados. A função retorna o processamento para o bloco que a chamou trazendo apenas o valor de retorno. A única maneira de fazer com que a função modifique o valor de alguma variável definida no bloco de programa que a chamou é passá-la por um ponteiro com o seu endereço.

Vejamos o fragmento de código seguinte:

       int f( int x ) 
          {
           x--;
           return x;
          }

       int main()
          {
           int a = 10;
           int b;

           b = f(a);
           ...
           ...

Em "C", a menos que o programador seja bem malicioso e faça manipulações de memória arriscadas, a função f jamais alterará o valor do seu argumento x.

Diferentemente da linguagem "C", a chamada a uma função em C++ pode alterar o valor de uma variável definida antes da chamada da função, mesmo sem esta variável ser explicitamente passada como um ponteiro. Este modo é chamado de passagem por referência. Em termos mais gerais, significa a passagem da variável propriamente dita, para o corpo interno da função com outro nome, aquele definido na lista de parâmetros da função.

Em C++, uma função pode ser chamada na forma acima e alterar o valor das suas variáveis. Para isso basta declará-la como:

  int f(int & x) 
  {
    x--;
    return x;
  }

Temos em C++ o operador & que se comporta analogamente do mesmo em C, porém tendo uma função a mais, a de criar variáveis de referência:

  • &x quando usado no código retorna o pointero para o endereço de x;
  • &x quando usado na declaração de variável, cria uma referência;
  • &x quando usado como parâmetro na declaração de uma função faz com que suas chamadas transfira o argumento/parâmetro passando-o de forma similar a passagem de seu ponteiro. (Passagem por referência).

Em termos semânticos, ao passar a variável para uma função onde o parâmetro é uma referência, o endereço da variável é atribuído ao endereço do parâmetro. Desta forma, o parâmetro é a mesma variável passada, no trecho de código onde a função foi invocada, assumindo um nome diferente dentro da função. Podemos dizer que a variável assume um apelídio dentro da função, sendo a mesma com nome diferente apenas.

Vejamos um exemplo usando a função anterior:

  int m = 4;
  f(m);
  cout << m << endl;

O código anterior imprime na saída padrão o valor 3. Acompanhando o fluxo de execução verificamos o seguinte: Depois que a variável m é incluída na chamada da função o seu nome muda para x e o programa passa a ser executado dentro da função, onde a variável é decrementada. Portanto, quando a execução retorna para o corpo principal a variável estará decrementada.

Exemplo: alterando o valor da variável usando referência

editar
 #include <iostream>
 using namespace std;
 int main()
 {
    int val = 1;
    int &ref = val;
    cout << "val is " << val << endl;
    cout << "ref is " << ref << endl;
    cout << "Setting val to 2" << endl;
    val = 2;
    cout << "val is " << val << endl;
    cout << "ref is " << ref << endl;
    cout << "Setting ref to 3" << endl;
    ref = 3;
    cout << "val is " << val << endl;
    cout << "ref is " << ref << endl;
    cout<<"Digite enter para continuar..."<<endl;
    cin.get();
    return 0;
 }

Como se viu conseguimos alterar o valor de val alterando o valor de ref.

Existe apenas umas restrições para o seu uso:

  • Teremos de inicializar e no momento da declaração teremos de atribuir de imediato o valor (se não fizermos isso gerará um erro de compilação)
  • As referência não podem ser reatribuídas, ou seja no exemplo anterior tinha

int &ref = val; se mais tarde no código tentar-se fazer int &ref=m; (sendo m uma variável já declarada e iniciada por hipótese) o que acontece é que a 2ª instrução é completamente ignorada e ficamos sempre com a primeira.

A vantagem real das referências é que quando elas são usadas para passar valores para as funções elas providenciam uma maneira de retornar valores das funções.

Vejamos o exemplo

 #include <iostream>
 using namespace std;
 int main()
 {
    int val1 = 10;
    int val2 = 20;
    int &ref = val1;
    cout << "val1 is " << val1 << endl;
    cout << "val2 is " << val2 << endl;
    cout << "ref is " << ref << endl;
    ref = val2;    //What does this do?
    cout << endl << "ref = val2" << endl;
    cout << "val1 is " << val1 << endl;
    cout << "val2 is " << val2 << endl;
    cout << "ref is " << ref << endl;
    val2 = 30;
    cout << endl << "Setting val2 = 30" << endl;
    cout << "val1 is " << val1 << endl;
    cout << "val2 is " << val2 << endl;
    cout << "ref is " << ref << endl;
    cout<<"Digite enter para continuar..."<<endl;
    cin.get();
    return 0;
 }


Exemplo: Swap

editar

O exemplo abaixo mostra uma forma muito comum de usar referências. A instrução "swap", que tem por objetivo trocar os valores de duas variáveis, é mais naturalmente chamada como Swap(a, b) do que Swap(&a, &b); assim, é mais simples declarar a função usando referência:

 #include <iostream>
 using namespace std; 
 void Swap (int &i,int &j)
  {
 	int t=i;
 	i=j;
 	j=t;
  }
  int main ()
  {
 	int a,b; 
 	a=5;
 	b=10;
 	cout<<a<<"\t"<<b;
 	Swap (a,b);
 	cout<<a<<"\t"<<b;
        cout<<"Digite enter para continuar..."<<endl;
        cin.get();
     	return 0;
  }

Comparação entre passagem por referência e ponteiros

editar

Para exercitar vamos criar um novo problema: Criar um função que duplique qualquer valor colocado pelo utilizador:

1º PROGRAMA-via referência 2º PROGRAMA – via ponteiros - endereços
 #include <iostream>
 using namespace std;
 void doubleIt(int&);//prototype com endereço de variavel
 int main ()
 {
   int num;
   cout << "Enter number: ";
   cin >> num;
   cin.get();
   doubleIt(num);	//chamo função, passando parametro num
   cout << "The number doubled in main is " << num << endl;
   cout<<"Digite enter para continuar..."<<endl;
   cin.get();
   return 0;
 }
 void doubleIt (int& x)	 	
 {
   cout << "The number to be doubled is " << x << endl;
   x *= 2;
   cout << "The number doubled in doubleIt is " << x << endl;
 }
 #include <iostream>
 using namespace std;
 void doubleIt(int*);  //parametro por endereço
 int main ()
 {
   int num;
   cout << "Enter number: ";
   cin >> num;
   cin.get();
   doubleIt(&num);//passei parametro como endereço
   cout << "The number doubled in main is " << num << endl;
   cout<<"Digite enter para continuar..."<<endl;
   cin.get();
   return 0;
 }
 void doubleIt (int* x)
 {
   cout << "The number to be doubled is " << *x << endl;
   *x *= 2;
   cout << "The number doubled in doubleIt is " << *x << endl;
 }


Ou seja, nestes dois códigos temos uma passagem por referência e outro por endereço. Com diferenças:

  • Na chamada da função (dentro do main() )
doubleIt(num);   // por referência
doubleIt(&num);  // por endereço
  • No protótipo da função (confirmar o ponto e virgula)
void doubleIt(int&);  // por referência
void doubleIt(int*);  // por endereço
  • No "header" da função
void doubleIt (int& x)   // por referência
void doubleIt (int* x)   // por endereço
  • dentro do corpo da função (dentro da própria função)
x
*x

Podemos pensar que passando por referência parece ser muito mais simples do que passado por endereço.

Na verdade existem certas funções da biblioteca que só permitem a passagem por endereço.

Entrada e saída

editar

Aqui vamos dar início ao estudo de recursos que possibilitarão inserir dados e fazer reporte da falta deles.

No C++ a entrada e saída podem ser feitas através da biblioteca iostream. Para podermos usá-la deveremos colocar a linha de código: #include <iostream>

A estrutura de comunicação com o meio externo em modo texto é composta por um conjunto de objetos. Estas, em conjunto com operadores e funções de formatação possibilitam uma forma de comunicação mais intuitiva. Devido à abstração de elementos do mundo real por recursos da orientação a objetos, a forma de entender o código torna-se mais natural.

Na biblioteca iostream, temos os seguintes objetos:

  • cin - Este objeto fornece entrada de dados "bufferizada" através do "standard input device", o dispositivo de entrada padrão;
  • cout - Este objeto fornece saída de dados "bufferizada" através do "standard output device", o dispositivo de saída padrão;
  • cerr - Este objeto fornece saída de dados não "bufferizada" para o standard error device, o dispositivo de erro padrão, que é inicialmente definido para a tela.
  • clog - Este objeto fornece saída "bufferizada" através do "standard error device", o dispositivo de erro padrão que é inicialmente definido para a tela.

O foco de orientação a objetos que a biblioteca iostream confere aos dispositivos de entrada e saída é uma das características da linguagem C++. Ele está presente na maneira na qual o código foi idealizado e está formatado, modificando a maneira como as partes do sistema de entrada/saída interagem. Desta forma, as operações de interação entre o usuário e o software tornam-se mais intuitivas para o programador.

O sistema de entrada e saída é um exemplo deste modelo de programação, onde cada entidade física ou lógica de entrada e saída é representada por objetos cujas operações podem ser acessadas diretamente nos programas.

Buffer

editar

Para entendermos um pouco mais sobre Buffer, se faz necessário recordar um pouco sobre o funcionamento da memória e suas operações relacionadas a Buffer.

Bufferização é um meio de sincronização entre dispositivos de velocidades diferentes, tais quais memória e dispositivos de armazenamento mecânicos, como discos magnéticos. Para evitar que as operações do dispositivo mais lento interfiram no desempenho do programa pode-se fazer com que os dados sejam colocados em uma memória mais rápida e depois sejam enviadas ao dispositivo mais lento a medida que ele tenha disponibilidade para recebê-los, desta forma temos os seguintes modos de escrita em dispositivos de saída:

  • unbuffered – significa que qualquer mensagem ou dados serão escritos imediatamente. É o caso da escrita no dispositivo cerr;
  • buffered - significa que os dados serão mantidos num buffer de memória até que o dispositivo de destino solicite, ou que um comando de descarregamento seja executado, ou quando o buffer estiver cheio. O problema é que se o programa é interrompido antes do buffer ser escrito esses dados são perdidos.
cout << "hello";	// mostra a palavra hello no ecrã(monitor)
cout << 120; 		// mostra o número 120 no ecrã(monitor)
cout << hello;	// mostra o conteúdo do pedaço de memoria a que chamamos de "hello" no ecrã(monitor)
cout << "hello, tenho " << age<< " anos de idade"; 	/* mostra a primeira string depois 
                                                           vai buscar o conteúdo da variável 
                                                           age de depois a string “anos de idade”
                                                        */
cout << "Primeira frase. ";
cout << "Segunda frase.\n" << "Terceira frase."; /* imprime:
                                                     Primeira frase. Segunda frase.
                                                     Terceira frase.
                                                  */


O cout (c+out) usado em conjugação com o operador de inserção “<<” permite enviar dados para o "stream out" que por definição é o ecrã (monitor).

Então podemos enviar as constantes, as variáveis, a conjugação das duas se nos apetecer, separadas pelo operador de inserção.

Temos ainda diversos recursos de formatação através de "escapes sequences" que detalharemos no tópico logo a seguir, o recurso usado aqui concatena as várias frases na mesma linha. Temos de dizer explicitamente "quebra de linha", através do "\n", que faz com que a sequência logo após, seja escrita na próxima linha.

Uma característica muito importante do C++, presente nas instruções logo acima, é o polimorfismo notável na operação de apresentação dos dados na saída; Note que os tipos de dados que são passados para o cout são diversos, ou seja, não importa qual o tipo de dado que será entregue ao cout, de alguma maneira ele sempre formatará de uma maneira legível no monitor. Nos capítulos mais adiante veremos como fazer com que tipos de dados diferentes sejam tratados pelo mesmo objeto.

Escape Sequences

editar

Há um conjunto de caracteres, nós chamamos de string. Mas no exemplo anterior quando usamos o "\n", nós antes dissemos que o cout com o operador << iria colocar no monitor todos os caracteres que estivessem entre aspas. Acontece que existem estas strings especiais – chamadas de "escape sequences" - que de alguma forma alteram o sentido das strings. Existem muitas destas sequências. As mais conhecidas são estas:


Escape Sequences (as mais comuns)

  • \n nova linha muda o cursor para uma linha abaixo
  • \r retorno
  • \t tabulador muda o cursor para o próximo ponto de tabulação
  • \v tabulador vertical
  • \b deleção reversa
  • \f alimentador de página
  • \a alerta (bipe) faz o computador emitir um sinal sonoro
  • \' aspas simples (') imprime aspas simples
  • \" aspas duplas (") imprime aspas duplas
  • \? sinal de interrogação (?)
  • \\ barra oposta (contrabarra) (\)

O objeto cin obtém informação do "standard input" (que usualmente é o teclado). Este objeto está tal como o cout declarado no cabeçalho da biblioteca <iostream>

A sintaxe mais comum da instrução para obter dados do cin é:

  • cin >> [variable name];

Aqui temos o operador de extração ">>" que diz que tudo o que o teclado escrever, coloque esses dados na variável que me segue. Este operador consegue até traduzir o conceito de dados de fora para dentro.

#include <iostream>

using namespace std;

int main(void)
 {
   int testScore;
   cin >> testScore;
   cout << "Your test score is " << testScore << "\n";

#ifdef WIN32
   system ("pause"); /* Necessário apenas para sistemas Microsoft®, em modo gráfico.
                        Em UNIX®, variantes e similares use um terminal de texto e 
                        esta função não será necessária. */
#endif

   return 0;
 }

Há mais um pormenor. O computador está á espera de um "Return" ("ENTER", ou "New Line", ou Espaço em Branco ) para finalizar a entrada de dados na variável, até lá o cursor aparece a piscar.

Bem, na verdade, este ponto é muito importante, por que… Vejamos mais a baixo a questão de termos 2 entradas.


Pergunta: declaramos uma variável int testScore e se colocarmos um valor que não seja um int? Isto não é a mesma situação do capítulo anterior porque antes o programa ainda não tinha compilado, e agora temos entrada de dados quando o programa já está compilado e a correr/rodar.

Assim se no exemplo anterior colocarmos o nome “Jeff”, que é uma string, e o programa está a espera de um int, o que acontece é que o cin não vai colocar "jeff" na variável (ele ignora a entrada). E quando o cout é chamado ele vai colocar o valor que está na variável.

Então porque é que me apareceu o número –858993460 quando corri/rodei o programa? É que na memória física do computador existem dados da área onde a variável está alocada fisicamente, e quando declarei o testScore o compilador apenas reserva aquela memória mas não apaga o que lá está.

Pergunta: O que é que acontece quando inserimos um número maior do que o limite do tipo quando o programa executar? É o caso de "overflow" - estouro de memória, mas quando o programa corre/roda.

Aqui já não temos a questão de dar a volta ao intervalo permitido, aqui temos o caso em que vai ser colocado um número estranho. Isto não está perfeitamente explicado.

Chama-se prompt quando é dito ao utilizador o que deve fazer, o que não deixar como no exemplo anterior o cursor a piscar sem o utilizador saber o que fazer. Além de que temos o problema de overflow em execução, portanto é bom que o utilizador cumpra os requerimentos.

De uma maneira muito conveniente, os dados recebidos pelo cin são tratados de forma a tornar o processo polimórfico, da mesma forma que no caso de cout, assim temos como receber os dados da maneira que precisamos, ou seja, quando declaramos uma variável int e a usamos para receber um dado do cin, o mesmo é convertido na entrada para inteiro, quando usamos uma variável de outro tipo, a entrada é convertida para o tipo da variável.

Lendo um caractere

editar

Ler um caractere até é simples, basta utilizar o objeto cin e será guardado o valor digitado na variável.

   char nivel;
   cout << "Entre um nível: ";
   cin >> nivel;

Porém teremos de pressionar a tecla ENTER depois de digitar o caractere. Isto leva a várias questões.


O problema “pressione uma tecla para continuar...”

 #include <iostream>

 using namespace std;

 int main(void)
 {
   char ch;
   do {
      cout << "Pressione S ou s para sair, qualquer outra tecla para continuar: "; 
      cin >> ch; 
      if (ch != 'S' && ch != 's')
         cout << "Deseja continuar?"<<endl;
      else
         cout << "Saindo..."<<endl;
   } while (ch != 'S' && ch != 's');

#ifdef WIN32
  system ("pause");
#endif

  return 0;
 }
  • O programa funciona bem se pressionarmos S ou s para sairmos;
  • O programa funciona bem se pressionarmos qualquer outra tecla com caractere imprimível;
  • Mas se pressionarmos a tecla ENTER, nada acontece, o cin continua à espera de entrada. A razão é o operador de extração ">>" ignora os espaços em branco e os caracteres "nova linha" resultantes do pressionamento da tecla enter.

A função cin.get()

editar

Já tivemos oportunidade para discutir a função getline (função membro) do objeto cin.

cin.getline(name,80);

Aqui vamos utilizar uma outra função, a cin.get().

Esta função pode ser chamada, tal como a getline(), através de 3 argumentos, onde o primeiro é o array de caracteres, mas também o pode ser sem argumentos ou ainda apenas um argumento.

No caso de não conter argumentos apenas irá ler um caractere, em vez de uma cadeia de caracteres.

No caso de ter um argumento, ela aceita qualquer tecla incluindo o enter. (o que não se passa com o cin e o operador de extração). Aqui um exemplo

 #include <iostream>

 using namespace std;
 
 int main(void)
 {
   char ch;
   do {
      cout << "Pressione S ou s para sair, \nqualquer outra tecla para continuar: "; 
      cin.get(ch);
      if (ch != 'S' && ch != 's')
         cout << "Deseja continuar?"<<endl;
      else
         cout << "Saindo..."<<endl;
   } while (ch != 'S' && ch != 's');

#ifdef WIN32
  system ("pause");
#endif


   return 0;
 }

Porém se pressionarmos uma tecla de caractere imprimível, não conseguiremos inserir o próximo prompt, parece que houve um salto. Estranho!

Para explicar a razão deste novo problema necessitamos de explicar o conceito de buffer.

O "input buffer" é uma área de memória que guarda os caracteres de entrada, por exemplo do telado, até que essa entrada seja atribuída pelo cin e o operador de extração >>, ou por funções como get() ou getline() do objeto cin.

Quando o loop começa, o "input buffer" está vazio.

  • Se digitarmos apenas o enter, sendo este o primeiro e único caractere no "imput buffer", ele é removido do input buffer e atribuído á variável ch, então o "input buffer" está vazio na próxima iteração do loop;
  • Se digitarmos x e enter. Temos 2 caracteres. A função get() retira o primeiro caractere do "input buffer" e atribui à variável ch, mas nisto o caractere nova linha permanece no "input buffer". Isto faz com que na próxima iteração do loop, não haja a oportunidade para entrar com dados.

Ou seja, na segunda iteração, é retirado o caractere nova linha – que ficou da 1ª iteração - e é colocado na variável ch. Agora o "input buffer" está vazio.

cin.ignore()

editar

Uma solução é limpar o caractere nova linha do "input buffer" antes da chamada da função getline(). E fazemos isso usando a função ignore() do objeto cin.

Esta função membro tal com a get() e a getline() são sobrecarregadas, podem ser chamadas sem argumentos, com um ou dois argumentos.

Utilizar a função ignore() sem argumentos, permite que o próximo caractere no "input buffer" seja lido e depois descartado,- e isto é exatamente aquilo que queríamos.

A função com 1 ou 2 argumentos é usada para cadeias de caracteres.

  • Com um argumento, o argumento é o número máximo de caracteres a ser removido do "input buffer". Exemplo:
 cin.ignore(80); // Remove até 80caracteres do input buffer
  • Com dois argumentos, o segundo argumento é o delimitador, um caractere que se encontrado, antes do número de caracteres especificado no primeiro paramento, faz com que a remoção pare. Exemplo:
cin.ignore (80, '\n'); // Remove 80 caracteres se até lá não encontrar o nova linha.

Reescrevendo o código anterior utilizando o cin.ignore()

 #include <iostream>

 using namespace std;

 int main(void)
 {
   char ch;
   do {
      cout << "Pressione S ou s para sair,\n qualquer outra tecla para continuar: "; 
      cin.get(ch);
      cin.ignore();
      if (ch != 'S' && ch != 's')
         cout << "Deseja continuar?"<<endl;
      else
         cout << "Saindo..."<<endl;
   } while (ch != 'S' && ch != 's');

#ifdef WIN32
  system ("pause");
#endif


   return 0;
 }

Ora este programa funciona muito bem, MAS…

Se pressionarmos a tecla Enter para continuar, teremos de fazer isso duas vezes, pois a primeira vez é ignorada. A razão: é que não existe nada no "input buffer" quando a função ignore é chamada, por isso é que a tecla enter necessita de ser pressionada 2 vezes, colocando um caractere nova linha a mais no "buffer" para a função ignore() remover.

Se tentarmos modificar isto através do if?

 #include <iostream>

 using namespace std;

 int main(void)
 {
   char ch;
   do {
      cout << "Pressionar S ou s para sair,\n qualquer outra tecla para continuar: "; 
      cin.get(ch);
      if (ch != '\n')
         cin.ignore();
      if (ch != 'S' && ch != 's')			
         cout << "Deseja continuar?"<<endl;
      else
         cout << "Saindo..."<<endl;
   } while (ch != 'S' && ch != 's');

#ifdef WIN32
  system ("pause");
#endif

   return 0;
 }

Agora sim temos todos os problemas resolvidos e isto agora funciona!!

cin, cin.get(), cin.getline()

editar

O problema anterior do caractere nova linha permanece quando usamos o cin, o get() e o getline() juntos num programa, uma vez que o enter é usado para terminar a entrada.

 #include <iostream>

 using namespace std;

 int main(void)
 { char name[80];
   int courseNum;
   cout << "Informe o número do curso: ";
   cin >> courseNum;
   cout << "Informe seu nome: ";
   cin.getline(name, 80);
   cout << "O número do curso é: " << courseNum << endl;
   cout << "Seu nome é: " << name << endl;

#ifdef WIN32
  system ("pause");
#endif

   return 0;
 }

Aqui, neste exemplo, nós não tivemos a oportunidade de colocar o nome. Quando digitamos o número e depois pressionamos a tecla enter, o cin coloca o número no courseNUm mas o caractere nova linha permanece no "input buffer", que fica para o enter name, pois o getline lê espaços em branco.

A solução pode ser:

 #include <iostream>

 using namespace std;

 int main(void)
 {
   char name[80];
   int courseNum;
   cout << "Informe o número do curso: ";
   cin >> courseNum;
   cin.ignore();
   cout << "Informe seu nome: ";
   cin.getline(name, 80);
   cout << "O número do curso é: " << courseNum << endl;
   cout << "Seu nome é: " << name << endl;

#ifdef WIN32
  system ("pause");
#endif

   return 0;
 }

A partir destes exemplos podemos criar umas regras:

  1. Colocar sempre a função ignore() depois do cin e do >>;
    • Razão: O "cin>>" deixa sempre o nova linha no "input buffer". Assim devemos eliminá-lo com a função ignore().
  2. Não colocar a função ignore(), no caso de ser sem parâmetros, depois do getline();
    • Razão:O getline() remove o caractere nova linha que termina a entrada do "input buffer", portanto não é necessário o ignore().
  3. Verificar se temos o caractere nova linha no "input buffer" depois de utilizar o get(), se tivermos deveremos utilizar o ignore().
    • Razão: A função get() com um argumento deixa o caractere nova linha no "input buffer" se pressionarmos um caractere e o enter. mas não deixará, se apenas pressionarmos o enter. portanto é necessário confirmar.

Entrada de valores para variáveis múltiplas

editar

Podemos fazer com que o programa receba vários valores ao mesmo tempo

cin >> [first variable] >> [second variable] >> [third variable];

Neste caso o utilizador separa os dados por espaços (o enter também dá) e como anteriormente o utilizador fecha utilizando o enter

 #include <iostream>
 #include <string> 

 using namespace std;

 int main(void)
 {
   int peso, altura;
   string nome;

   cout << "escreva o seu nome, peso e altura \n separe os valores por espaços\n";
   cin >> nome >> peso >> altura;
   cout << "o seu nome é:" << nome << "\n";
   cout << "o seu peso é:" << peso << "\n";
   cout << "a sua altura é: " << altura<< "\n";

#ifdef WIN32
   system ("pause"); /* Necessário apenas para sistemas Microsoft®, em modo gráfico.
                        Em UNIX®, variantes e similares use um terminal de texto e 
                        esta função não será necessária. */
#endif

   return 0;
 }

Note-se que no exemplo anterior poderíamos colocar as 3 variáveis, mesmo no caso de elas serem de tipos diferentes. Temos de ter atenção na ordem de entrada.

Pergunta: se escrevêssemos duas palavras para o nome, apenas a primeira é que apareceria a 2 palavra que estava separada da primeira para um espaço seria colocada na variável peso.

Entrada/Saída em ficheiros (arquivos)

editar

Nota introdutória: Este capitulo geralmente é colocado uns capítulos mais para o fim, mas acho por bem que se torne as coisas mais interativas, o que é possível introduzindo agora operações em arquivos, dá muito mais entusiasmo. Encontramos aqui conceitos avançados mas poderão ser deixados para depois se o leitor não quiser observar o tema neste momento.

Gravar (Salvar) os dados para um ficheiro(arquivo)

editar

Os dados que mantemos nos programas estão guardados na memória RAM, que é limpa quando o programa ou computador para de funcionar. Isso implicaria que perderíamos toda a informação! Porém existe uma maneira para tornar os dados persistentes que é gravar os dados num ficheiro (arquivo) no "hard drive" (disco rígido) ou no outro meio persistente. Nas formas mais diretas de escrita podemos passar os dados em formato binário para o ficheiro(arquivo). Outros meios avançados para guardar dados podem envolver bases de dados relacionais ou XML.

O que é um ficheiro(arquivo)?

editar

Um arquivo é uma coleção de dados que estão localizados numa memória persistente tipo hard drive, cd-rom, etc. Para identificarmos o arquivo podemos atribuir-lhe um nome (filename). Os "filenames" têm usualmente uma extensão, que determina o tipo de arquivo em sistemas operacionais semelhantes aos da Microsoft®, mas que podem ser dispensados em sistemas operacionais que guardam as características dos arquivos no meio de armazenamento, tais quais sistemas UNIX® e seus similares GNU/Linux, FreeBSD, etc... A extensão é representada por 3 ou 4 letras que seguem após o nome do arquivo e um ponto ".". Por exemplo: "joao.doc" ou "joao.odt". Isto diz-me que temos um ficheiro(arquivo) que se chama "joao", e que tem a extensão .doc que refere usualmente a documentos do WORD no primeiro caso e com extensão ".odt" do OpenOffice no segundo. Outros tipos de extensões podem ser ".xls" para documentos EXCEL, ".ods" para planilhas do OpenOffice. ou ainda ".cpp" para ficheiros(arquivos) de códigos de c++.

Ficheiros(Arquivos) binários e tipo texto

editar

Existem na verdade dois tipos de ficheiros(arquivos): os do tipo texto e os do tipo binário.

  • Os arquivos tipo texto apenas armazenam texto obedecendo uma codificação de caracteres, a mais comum é a ASCII, isto implica o uso do código para armazenamento, ou seja, pode ser que a codificação seja interpretada antes de ser efetivada no meio de armazenamento.
  • Os arquivos binários podem guardar mais informação, como imagens, base de dados, programas…Por exemplo, editores de texto com formatação, como o OpenOffice e o Word, guardam os seus arquivos em formatos binários, porque eles possuem além do texto, informação acerca da formatação do texto, para as tabelas, as listas numeradas, tipo de fonte, etc... daí aparecerem os caracteres de formatação tipo ã6, ÌL, h5…

Os arquivos binários poderão ser mais bem explorados em um tópico avançado, vamos trabalhar inicialmente com arquivos tipo texto, que poderemos operar de maneira mais simplificada.

biblioteca padrão fstream

editar

Até agora temos usado a biblioteca iostream (i de input + o de output + stream), que suporta, entre várias funcionalidades, o objeto cin para ler da "standard input" (que é usualmente o teclado) e o objeto cout para "standard output" (que usualmente é o monitor)

Ora, agora queremos é ler e escrever para ficheiros(arquivos) e isso requer a biblioteca fstream (f de file + stream). Esta biblioteca define 3 novos tipos de classe:

  • ofstream (apenas para saída – "out to a file". serve para criar, manipular ficheiros (arquivos) e escrever, não serve para ler).
  • ifstream (apenas para entrada – "in from a file" . serve para ler ficheiros (arquivos), receber dados dos mesmos, não serve para criar nem escrever).
  • fstream (este conjuga os dois tipos anteriores, "input and output to file". cria ficheiros (arquivos), escreve e lê informação dos mesmos.

Abrir um ficheiro(arquivo)

editar

Um ficheiro(arquivo) deve ser aberto pelo programa para que o mesmo possa ser manipulado, a abertura do arquivo implica, entre outras coisas, atribuir um identificador que nos permita ter acesso aos seus dados. É necessário criar uma linha de comunicação entre o arquivo e o objeto stream.

Podemos recorrer a dois métodos para abrir um ficheiro (arquivo):

  1. Usando um construtor;
  2. Usando a função membro chamada de "open".

Usando o Construtor

editar

O construtor é uma função que é automaticamente chamada quando tentamos criar uma instância de um objeto.

fstream afile;	 	//é criado uma instância do fstream chamada de afile

Os construtores de objetos podem ser sobrecarregados, ou seja, para a mesma classe podemos ter um construtor sem argumentos, com um argumento, dois argumentos, etc. No exemplo anterior criamos um sem argumentos. Os construtores não retornam valores, geralmente o compilador reporta erro quando se declara funções que retornam valor e estas têm o mesmo nome da classe, pois este nome é reservado para os construtores. Vamos dar um exemplo com dois argumento:

ofstream outfile ("joao.doc", ios::out);

Chama o construtor com dois argumentos, criando uma instância de ofstream e abrindo o ficheiro(arquivo) "joao.doc" para operações de saída.

Usando a função membro "open"

editar

Esta função tem como primeiro argumento o nome e localização do ficheiro/(arquivo) a ser aberto, o segundo argumento especifica o modo de abertura.

Sobre a questão da localização existem 2 tipos, o "path" relativo e o "path" absoluto. Para este último indicamos o caminho todo: "c:\\....\\joao.doc" em sistemas Microsoft® ou "/home/joao/joao.odt" para sistemas UNIX® e similares. O "path" relativo dispensa essa descrição se o ficheiro/(arquivo) estiver (na mesma directoria)/(no mesmo diretório) que o programa.

Sobre a questão do modo de abertura temos as seguintes modalidades:

Modo do abertura sinalizador
(Flag)
Descrição
ios::app "Append mode" Todos os dados do arquivo são preservados e qualquer saída é escrita a partir do fim do arquivo.
ios::ate Se o arquivo já existe,o programa vai diretamente ao seu fim.O modo de escrita é então feito de forma aleatória.Usado normalmente com arquivos do modo binário(binary mode).
ios::binary "Binary mode" Informações são escritas na forma binária e não na forma textual(text mode).
ios::in "Input mode" Leitura de informações de arquivo(não irá criar um arquivo novo)
ios::out "Output mode" Informações serão escritas no arquivo.
ios::trunc Se o arquivo já existe,suas informações serão truncadas, outra forma de se dizer: deletadas e reescritas.


Os sinalizadores (flags) são números em potências da base binária, portanto podemos ter vários flags ao mesmo tempo se usarmos o operador unário para a operação "OU", como no exemplo abaixo:

ofstream outfile;		                   //crio o objeto outfile
outfile.open("students.dat", ios::binary | ios::app); /*chamo a função membro open do objeto, 
                                                        com o 1º parâmetro que é o nome do arquivo 
                                                        e o 2º o modo de abertura. */

Observe que estamos abrindo o arquivo "students.dat" em modo binário e ao mesmo tempo com o modo "append", isto significa que abriremos o arquivo e poderemos preservar o seu conteúdo anterior inserindo os novos dados no fim do arquivo.

Comparando os dois métodos (pela função membro e pelo construtor)

editar

O primeiro método é similar a ter

int age;
age=39;

O segundo método é similar a

int age=39;

A escolha do melhor método em cada situação depende do contexto em que estamos criando o código, geralmente quando já temos o objeto criado e ele está fechado podemos abrir um novo arquivo com ele e depois fechá-lo novamente, isto nos sugere que usemos a função open quando o objeto deve abrir arquivos diferentes em cada trecho de código, embora que possam surgir outras funcionalidades, dependendo de como o projeto foi idealizado.

Abrir um arquivo para leitura

editar

A história aqui é a mesma só tem uma diferença: é que no caso de leitura, não será criado nenhum ficheiro (arquivo) caso ele não exista.

 ifstream arq;			//cria objeto "arq"
 arq.open (joão.doc);		//chama função membro open ao objeto "arq", com o 
                                //parâmetro do nome do ficheiro

Poderíamos fazer o mesmo com o construtor:

 ifstream arq (joão.doc);

Ou ainda

 fstream bloco;
 bloco.open("joao.doc", ios::in)

ou ainda

 fstream b(joao.doc, ios::in)


Há mais uma nota a fazer, se quisermos ler e escrever, não podemos usar o ofstream e o ifstream ao mesmo tempo, teremos de usar o fstream. Teremos de fazer:

 fstream a (joão.doc, ios::in | ios::out);

Neste caso, o comportamento padrão é preservar o conteúdo do ficheiro (arquivo) ou criá-lo caso ele não exista.

Verificar se o ficheiro (arquivo) foi aberto.

editar

Vamos verificar o que acontece quando tentamos abrir um arquivo que não existe, a primeira versão do nosso exemplo observa o comportamento básico do fstream:

 #include <fstream>
 #include <iostream>

 using namespace std;

 int main ()
 {
   ifstream arq;		        //crio objeto "arq" da classe ifstream - leitura
   arq.open("joao.doc");		//chamo função membro open

    cout << "(arq) = " << arq << endl;	//imprime o objeto	
    cout << "(arq.fail()) = " << arq.fail() << endl;	//chamo função membro fail

#ifdef WIN32
   system ("pause");
#endif

   return 0;
 }

No caso do ficheiro (arquivo) “joao.doc” não existir:

(arq) = 00000000
(arq.fail()) = 1

No caso do ficheiro (arquivo) “joao.doc” existir no mesmo diretório que o programa:

(a) = 0012FE40
(a.fail()) = 0


Repare que o resultado é a impressão do endereço, do objeto a de ifstream. dá um ponteiro!!

Agora, vajamos um exemplo mais completo:

 #include <fstream>
 #include <iostream>

 using namespace std;

 int main ()
 {
   ifstream arq;		        //crio objeto "arq" da classe ifstream - leitura
   string str;

   arq.open("joao.doc");		//chamo função membro open

   if (arq.is_open() && arq.good())
    {
        arq >> str;
        cout << "conteúdo: \n " << str << endl;	//imprime o conteúdo do arquivo	

        arq.close();
    }

#ifdef WIN32
   system ("pause");
#endif

   return 0;
 }

Observe que aqui verificamos se o arquivo foi aberto com a função membro is_open() que retorna verdadeiro "true" caso o arquivo foi aberto, depois verificamos se o arquivo foi aberto satisfatoriamente através da função membro good(), que também retorna verdadeiro se o arquivo pode ser usado.

Fechar um ficheiro (arquivo)

editar

Devemos fechar depois de ler e/ou escrever. Mas por que, se o objeto do ficheiro irá ser fechado assim que o programa acabar? Porque estamos a utilizar recursos com um ficheiro (arquivo) aberto, porque alguns sistemas operativos (operacionais) limitam o nº de ficheiros (arquivos) abertos, e estando este aberto impede que outros se possam abrir e por fim porque se não fecharmos, outros programas não poderão abri-lo até que o fechemos. Este comportamento faz parte de um esquema de controle de acesso usado pelo sistema para assegurar que os arquivos não serão usados por processos diferentes ao mesmo tempo.

 ofstream outfile;
 outfile.open("students.dat");
 // ....
 outfile.close();

Vamos criar um exemplo mais real. Queremos criar um programa que escreva informação inserida pelo utilizador num ficheiro por nós escolhido

 #include <fstream>
 #include <iostream>

 using namespace std;

 int main ()
 {	
   char data[80];		//criamos um array de 80 caracteres
   ofstream outfile;		//criamos objeto da classe ofstream

   outfile.open("joao.doc");	//chamamos a função membro da classe para o objeto criado.
                                // Esta função membro cria o arquivo "joao.doc" 

   if (outfile.is_open() && outfile.good()) //verificamos se está tudo bem
    { cout << "digite o seu nome: ";	//imprime no ecrã (monitor) a frase 
      cin.getline(data, 80);		//chama função membro getline do objeto cin para 
                                        //ler o que foi escrito pelo usuário

      outfile << data << endl;		//coloca o array no objeto criado. 	
      outfile.close();			//fechamos o objeto.
    }

#ifdef WIN32
   system ("pause");
#endif

   return 0;
 }

Podemos ir ver o novo ficheiro/arquivo com o nome joao.doc e tem lá escrito aquilo que digitamos.

Agora vamos tentar ler o que escrevemos no documento criado.

 #include <fstream>
 #include <iostream>
 using namespace std;
 int main ()
 {
   char data[80];
   ifstream infile; 
   infile.open("joao.doc"); 

   if (infile.is_open() && infile.good()) //verificamos se está tudo bem
    { infile >> data; 		//colocamos os dados abertos no array
      cout << data << endl; 	
      infile.close();
    }

#ifdef WIN32
   system ("pause");
#endif
   return 0;
 }

Repare que se tivéssemos escrito duas palavras, apenas uma era apresentada (ela pára no primeiro whitespace), para isso necessitaríamos de repetir:

 #include <fstream>
 #include <iostream>
 using namespace std;
 int main ()
 {
   char data[80];
   ifstream infile; 
   infile.open("joao.doc"); 

   if (infile.is_open() && infile.good()) //verificamos se está tudo bem
    { infile >> data; 		//colocamos os dados abertos no array
      cout << data << endl; 
      infile >> data; 		//colocamos os dados abertos no array
      cout << data << endl;	
      infile.close();
    }

#ifdef WIN32
   system ("pause");
#endif

   return 0;
 }

Agora já obtemos 2 palavras e são apresentadas em linhas diferentes. Mas temos de arranjar um método para não estar a repetir constantemente, podemos fazer isso com

infile.getline(data, 80);


Então ficamos com:

 #include <fstream>
 #include <iostream>
 #include <string>
 using namespace std;
 int main ()
 {
   string data;
   ofstream outfile;
   outfile.open("joao.doc");
   if (outfile.is_open() && outfile.good()) //verificamos se está tudo bem
    {
      cout << "Writing to the file" << endl;
      cout << "===================" << endl; 
      cout << "Enter class name: "; 
      getline(cin, data); 
      outfile << data << endl;
      cout << "Enter number of students: "; 
      cin >> data;
      cin.ignore();		//esta função membro é para limpar o caractere 
                                //newline do inputbuffer depois de usar o objeto 
                                //cin com o operador de extração >>
      outfile << data<< endl;
      outfile.close();
     }
 
   ifstream infile; 

   infile.open("joao.doc "); 

   if (infile.is_open() && infile.good()) //verificamos se está tudo bem
    { cout << "Reading from the file" << endl; 
      cout << "=====================" << endl; 
      getline(infile, data);
      cout << data << endl; 
      getline(infile, data);
      cout << data << endl; 
      infile.close();
    }

#ifdef WIN32
   system ("pause");
#endif

   return 0;
}

Looping pelo ficheiro (arquivo).

editar

E se não soubermos quantas linhas tem o arquivo? O objeto ifstream tem uma função membro que é a eof() (e-end+o-of+f-file). Esta função não tem parâmetros e retorna "true" se o fim do arquivo for alcançado e "false" caso contrário. No entanto, esta função eof() não é de confiança com os ficheiros (arquivos) texto como o é para os binários (é que nos ficheiros (arquivos) binários não existem espaços em branco).

A melhor alternativa é usar a função membro fail().

 ifstream infile;
 infile.open("joao.doc");

 if (infile.is_open() && infile.good())
  { infile >> data;

    while(!infile.fail())
     { infile >> data;
       cout << data;
     }

    infile.close();
  }


Refazendo tudo

 #include <fstream>
 #include <iostream>
 #include <string>

 using namespace std; 

 int main ()
 {
    string data;
    ofstream outfile;
    outfile.open("joao.doc");

  if (outfile.is_open() && outfile.good())
   { 
     cout << "Escrevendo no arquivo" << endl;
     cout << "===================" << endl; 
     cout << "Informe o nome da classe: "; 
     getline(cin, data); 
     outfile << data<< endl;
     cout << "informe o número de estudantes: "; 
     cin >> data;
     cin.ignore();
     outfile << data<< endl;
     outfile.close(); 
   }

    ifstream infile; 
    infile.open("joao.doc"); 

 if (infile.is_open() && infile.good())
  { 
    cout << "Lendo do arquivo" << endl; 
    cout << "=====================" << endl; 
    getline(infile, data);

    while(!infile.fail())
    {
       cout << data << endl; 
       getline(infile, data);
    }

    infile.close();
  }

#ifdef WIN32
    system ("pause");
#endif

    return 0;
 }


Agora vamos fazer o nosso programa mais modular:

  1. . writeFile – para abrir um arquivo para escrita usando o ofstream e
  2. . readFile - ler do ficheiro (arquivo) usando o ifstream
  3. . Cada função irá verificar se o ficheiro (arquivo) foi aberto com sucesso
 #include <fstream>
 #include <iostream>
 #include <string>

 using namespace std;

 bool writeFile (ofstream&, char*);
 bool readFile (ifstream&, char*); 

 int main ()
 {
    string data;
    bool status; 
    ofstream outfile;

    status = writeFile(outfile, "students.dat");
    if (!status)
    {
       cout << "Arquivo não pode ser aberto para escrita.\n";
       cout << "Programa terminando...\n";
       return 0;
    }
    else
    {
       cout << "Escrevendo no arquivo" << endl;
       cout << "===================" << endl; 
       cout << "Informe o nome da classe: "; 
       getline(cin, data); 
       outfile << data<< endl;
       cout << "Informe o número de estudantes: "; 
       cin >> data;
       cin.ignore();
       outfile << data<< endl;
       outfile.close();
    }
  
    ifstream infile;
    status = readFile(infile, "students.dat");
    if (!status)
    {
       cout << "O arquivo não pode ser aberto para leitura.\n";
       cout << "Programa terminando...\n";
       return 0;
    }
    else
    {
       cout << "Lendo do arquivo" << endl; 
       cout << "=====================" << endl; 
       getline(infile, data);
       while(!infile.fail())
       {
          cout << data << endl; 
          getline(infile, data);
       }
       infile.close();
    }

#ifdef WIN32
    system ("pause");
#endif

    return 0;
 }

 bool writeFile (ofstream& file, char* strFile)
 {
    file.open(strFile);
    return !(file.fail()||!file.is_open()||!file.good());
 }

 bool readFile (ifstream& ifile, char* strFile)
 {
    ifile.open(strFile);
    return !(ifile.fail()||!ifile.is_open()||!ifile.good());
 }

Manipuladores

editar

Os objetos das classes "stream" podem ser configurados para fornecer e reportar os dados de maneira pré-formatada. Da mesma maneira que temos a formatação quando usamos funções de formatação, como printf() e scanf(), na linguagem C, podemos usar os manipuladores na linguagem C++ para informar os objetos streams em que formato desejamos receber os dados deles ou fornecer para eles.

Abaixo temos uma série de manipuladores úteis:

Manipulator Uso
boolalpha Faz com que variáveis tipo bool sejam reportadas como "true" ou "false".
noboolalhpa (padrão) Faz com que variáveis tipo bool sejam reportadas omo 0 ou 1.
dec (padrão) Determina que variáveis tipo inteiras (int) sejam reportadas na base 10.
hex Determina que variáveis tipo inteiras (int) sejam reportadas em hexadecimal.
oct Determina que variáveis tipo inteiras (int) sejam reportadas em octal.
left Faz com que textos sejam justificados a esquerda no campo de saída.
right Faz com que textos sejam justificados a direita no campo de saída.
internal Faz com que o sinal de um número seja justificado a esquerda e o número seja justificado a direita.
noshowbase (padrão) Desativa a exibição do prefixo que indica a base do número.
showbase Ativa a exibição do prefixo que indica a base do número.
noshowpoint (padrão) Mostra o ponto decimal apenas se uma parte fracionária existe.
showpoint Mostra o ponto decimal sempre.
noshowpos (padrão) Nenhum sinal "+" prefixado em números positivos.
showpos Mostra um sinal "+" prefixado em números positivos.
skipws (padrão) Faz com que espaços em branco, tabulações, novas linhas "\n" sejam descartados pelo operador de entrada >>.
noskipws Faz com que espaços em branco, tabulações, novas linhas "\n" não sejam descartados pelo operador de entrada >>
fixed (padrão) Faz com que números com ponto flutuante sejam mostrados em notação fixa.
Scientific Faz com que números com ponto flutuante sejam mostrados em notação científica.
nouppercase (padrão) 0x é mostrado para números em hexadecimal e para notação científica.
uppercase 0X é mostrado para números em hexadecimal e para notação científica.



Ajustando a largura da entrada/saída

editar
  • setw(w) - Ajusta a largura da saída e entrada para w; precisa ser incluído.
  • width(w) - Uma função membro das classes iostream.

Preenchimento de espaços em branco

editar
  • setfill(ch) - Preenche os espaços em branco em campos de saída com ch; precisa ser incluído.
  • fill(ch) - Uma função membro das classes iostream.

Ajustando a precisão

editar
  • setprecision(n) - Ajusta a precisão de casas decimais em números com ponto flutuante, para n dígitos. Este ajuste é apenas visual, de forma que o manipulador não afeta o modo de cálculo do número pelo programa.

Exemplificando o uso de manipuladores:

 #include <iostream>
 #include <iomanip>
 #include <string>
 using namespace std;

 int main()
 {
    int intValue = 15;
 
    cout << "Número inteiro" << endl;
    cout << "Padrão: " << intValue << endl;
    cout << "Octal: " << oct << intValue << endl;
    cout << "Hexadecimal: " << hex << intValue << endl;
    cout << "Ativando showbase " << showbase << endl;
    cout << "Decimal: " << dec << intValue << endl;
    cout << "Octal: " << oct << intValue << endl;
    cout << "Hexadecimal: " << hex << intValue << endl;
    cout << "Desativando showbase " << noshowbase << endl;
    cout << endl;
 
    double doubleVal = 12.345678;
 
    cout << "Números com ponto flutuante" << endl;
    cout << "Padrão: " << doubleVal << endl;
    cout << setprecision(10);
    cout << "Precisão de 10: " << doubleVal << endl;
    cout << scientific << "Notação científica: " << doubleVal << endl;
    cout << uppercase;
    cout << "Caixa alta: " << doubleVal << endl;
    cout << endl;
 
    bool theBool = true;
 
    cout << "Booleano" << endl;
    cout << "Padrão: " << theBool << endl;
    cout << boolalpha << "BoolAlpha ativo: " << theBool << endl;
    cout << endl;
 
    string myName = "John";
 
    cout << "Strings" << endl;
    cout << "Padrão: " << myName << endl;
    cout << setw(35) << right << "Com setw(35) e \"right\": " << myName << endl;
    cout.width(20);
    cout << "Com width(20): " << myName << endl;
    cout << endl;

#ifdef WIN32
    system ("pause");
#endif

    return 0;
 }

Exercícios

editar
  1. Quero colocar num documento, uma lista das combinações possíveis entre a,b,c e d. com a respectiva ordenação e quantidade;
  2. . Quero que seja a pessoa a escolher o nome do ficheiro (arquivo) e escrever também a localização;
  3. . Quero que seja depois tansformado num sistema modular;
  4. . Encontrar uma maneira para contar o nº de espaços em branco, o nº de caracteres "." que quisermos de um dado documento.
 #include <iostream>
 #include <fstream>

 using namespace std;

 int main()
 {
     int blank_count = 0;
     int char_count = 0;
     int sentence_count = 0;
     char ch; 
     ifstream object("jo.txt"); 
     if (! object)
     {
         cout << "Erro abrindo arquivo." << endl;
         return -1;
     } 
     while (object.get(ch))
     {
         switch (ch) 
         {
             case ' ': 
                 blank_count++;
                 break;
             case '\n':
             case '\t':
                 break;
             case '.':
                 sentence_count++;
                 break;
             default:
                 char_count++;
                 break;
         }
     }
     cout << "Existem " << blank_count << " espaços em branco;" << endl;
     cout << "Existem " << char_count << " caracteres;" << endl;
     cout << "Existem " << sentence_count << " sentenças." << endl;

#ifdef WIN32
     system ("pause");
#endif

     return 0;
 }

"Char strings" e Strings

editar

Os caracteres são entendidos como sendo números que geralmente têm oito bits, esses números são traduzidos na tabela ASCII de 128 caracteres, como existem inúmeras regiões no mundo com características linguísticas próprias, a tabela ASCII é estendida por um bloco de caracteres acima dos 128 mais baixos que varia de acordo com as necessidades de cada língua. A parte superior da tabela ASCII é conhecida como parte estendida e é referenciada por páginas de códigos para cada propósito linguístico, isso quer dizer que podemos ter os mesmos números significando caracteres diferentes para cada região do mundo.

No estilo da linguagem C quando queremos representar um conjunto de caracteres colocamos todos eles em uma matriz sequenciada na memória:

Endereço relativo 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09
Dado U m a f r a s e \0

Por exemplo, para declarar um espaço na memória que contenha 20 caracteres fazemos:

char dados[20];

Este é o estilo de strings usado pela linguagem C pura. Para manipular este tipo de string é preciso ter certo cuidado, pois a matriz sempre tem um tamanho definido e caso façamos um acesso a um endereço fora da matriz invadiremos outras áreas de memória que não temos como definir o que são, e portanto poderemos fazer o programa parar de funcionar, em muitos sistemas pode também haver danos aos outros programas e até mesmo ao próprio sistema operacional, porém em sistemas operacionais mais sofisticados como o GNU/Linux, que possuem gerenciamento de memória com proteção de memória, apenas o programa que causou a falha irá parar de funcionar.

Para manipular este tipo de string a biblioteca padrão da linguagem C dispõe de diversas funções, para mais detalhes consulte o livro Programar em C.

No estilo C++, como era de se esperar, as strings são objetos, eles podem ser criados facilmente através da biblioteca padrão referenciada pelo arquivo de cabeçalho <string>. As strings são objetos com recursos que permitem manipular os seus caracteres com as funcionalidades das funções da linguagem C e mais algumas características próprias possibilitadas pela orientação a objetos.

// listandoCodigoASCII.cpp

#include<iostream>
using std::cout;
using std::cin;
using std::endl;

#include<string>
using std::string;
using std::getline;


int main(){
  string anyWord;

  cout << "Digite uma palavra: ";
  getline(cin, anyWord);

  for ( int i = 0; i < anyWord.length(); i++)
    cout << anyWord[i] << " - " << (int)anyWord[i] << endl;
  cout << endl;

  return 0;

} //end main

Funções de caracteres úteis.

editar

As seguintes funções estão no cabeçalho da biblioteca <cctype>

toupper() – (to+upper) retorna a maiúscula de uma letra. é uma função de um argumento – o caractere. no caso do argumento não ser uma letra, a função retorna o mesmo caractere que é argumento.

tolower() – (to+lower) o mesmo comportamento que toupper(), porém com o resultado em minúscula.

 #include <iostream>
 #include <cctype>
 using namespace std;

 int main(void)
 {
   char ch;
   do {
      cout << "Pressionar S ou s para sair, \nqualquer outra tecla para continuar: "; 
      cin.get(ch);
      ch = toupper(ch);
      if (ch != '\n')
         cin.ignore();
      if (ch != 'S')
         cout << "Deseja continuar?\n";
      else
         cout << "Saindo...";
   } while (ch != 'S');

#ifdef WIN32
  system ("pause");
#endif

   return 0;
 }

Funções que verificam o caractere. Estas funções recebem apenas um argumento, o caractere e retornam um valor booleano.

Função Descrição
isalpha Retorna verdadeiro se o argumento é uma letra do alfabeto; falso em caso contrário.
isalnum Retorna verdadeiro se o argumento é uma letra do alfabeto ou um dígito; falso em caso contrário.
isdigit Retorna verdadeiro se o argumento é um dígito; falso em caso contrário.
islower Retorna verdadeiro se o argumento é uma letra minúscula, falso em caso contrário.
isprint Retorna verdadeiro se o argumento é um caractere imprimível (incluíndo espaços); falso em caso contrário.
ispunct Retorna verdadeiro se o argumento é um sinal de pontuação (caracteres imprimíveis que não sejam letras, dígitos ou espaço); falso em caso contrário.
isupper Retorna verdadeiro se o argumento é uma letra maiúscula; falso em caso contrário.
isspace Retorna verdadeiro se o argumento é um espaço, tabulação ou nova linha; falso em caso contrário.

Strings em C++

editar

As cadeias de caracteres da linguagem C podem formatar um novo tipo de dados, porém criar tipos de dados mais sofisticados não é possível nesta linguagem, as strings em C++ são objetos da classe string, o que isso traz de novo para o tratamento de textos em programas? A primeira coisa a notar quando criamos strings em C++ é a maneira de criá-las, a classe disponibiliza uma série de construtores:

1 string ( );
2 string ( const string& st );
3 string ( const string& st, size_t position, size_t n = npositions );
4 string ( const char * ps, size_t n );
5 string ( const char * ps );
6 string ( size_t n, char ch );

Isto torna possível, basicamente, criar string de seis maneiras diferentes:

  1. Podemos definir um objeto string vazio, para futuramente usarmos de acordo com a necessidade;
  2. Podemos criar um objeto string com uma cópia de outro;
  3. Podemos criar um objeto string com uma cópia de uma porção de outra string;
  4. Podemos criar um objeto string com uma cópia de uma parte de uma "char string";
  5. Podemos criar um objeto string com uma cópia de uma "char string";
  6. Podemos criar um objeto string preenchida com uma quantidade definida de um determinado caractere;

Quando manipulamos strings, podemos fazê-lo com operadores, como por exemplo "+", "+=", "<<", etc... Isto torna o código um pouco mais intuitivo, vejamos os operadores:

1 operator=
2 operator[]
3 operator+=
4 operator+
5 operator<<
6 operator>>

Que representam as operações:

  1. Atribuir o valor de uma string para outra;
  2. Acessar caracteres individualmente;
  3. Adicionar uma string no final de outra;
  4. Concatenar strings;
  5. Enviar uma string a um output stream;
  6. Receber uma string do input stream.

Apenas com estas poucas informações já é possível operar strings com bastante flexibilidade e de uma maneira muito intuitiva, vejamos alguns exemplos:

string a = ("Alice e Beto gostam de "),
       b = ("chocolate."),
       c = ("doce de leite."),
       d = ("pipoca."),
       e = (c);

cout << a + b << endl;
cout << a + c << endl;
cout << a + d << endl;
cout << a + e << endl;

Estas operações resultam em:

Alice e Beto gostam de chocolate. 
Alice e Beto gostam de doce de leite. 
Alice e Beto gostam de pipoca. 
Alice e Beto gostam de doce de leite.

Exemplos de como manipular strings em C++

editar

erase A função membro erase elimina parte de uma string. Os parâmetros passados para a função são a posição inicial e o número de caracteres a ser excluído. Veja um exemplo de uso abaixo:

  #include<iostream>
  using std::cout;
  using std::endl;
  using std::cin;

  #include<string>
  using std::string;
  using std::getline;

  int main(){
    string myText;

    cout << "Digite um texto qualquer" << endl;
    getline( cin, myText );
    myText.erase(7, 3);

    cout << myText << endl;

    return 0;
  }

Comparando formas de operar strings em C e C++

editar

Em C, temos diversas funções que são usadas para manipular strings, para mais detalhes veja o livro Programar em C, aqui faremos uma comparação dos modos de operar strings em C e C++, algumas particularidades da linguagem C++ permitem uma operação mais intuitiva das strings e algumas novas formas de tratá-las. Vejamos como manipular estes dados tão comuns em qualquer programa.

Funções uteis para o uso de strings

editar

strlen() – (str=string + len=length)- aceita um argumento que pode ser um array (uma cadeia) de caracteres, um ponteiro (que aponta para um array de caracteres) ou uma string literal. retorna um número inteiro que representa o número de caracteres, não incluindo o caractere "null":

 int len;
 len = strlen("Jeff") // a extensão é 4
 char* stinkydog = "Dante";
 len = strlen(stinkydog);   // a extensão é 5
 char name[80] = "Devvie";
 len = strlen(name);  // a extensão é 6

No c++ temos duas funções similares na classe string que são o lenght() e size(). Estas funções não tem argumentos pois reportam as informações sobre o objeto a quem pertencem, ambas retornam um inteiro que representa o tamanho das strings:

 string s = "Jeff Kent";
 cout << s.length();  // mostra: 9
 cout << s.size();    // também mostra: 9

Copiando strings

editar

Se tentássemos copiar strings desta maneira

 char* target = "Jeff Kent";
 char src[80] = "Micaela";
 target = src;

O que acontecia é que era a cópia do endereço de src para o ponteiro e não os caracteres que estão dentro da matriz.

No entanto existe a função strcpy (estilo C) – ela aceita dois argumentos,

  • O primeiro é para onde vai ser copiada e é passado o ponteiro desse array (não pode ser uma string literal).
  • O segundo é a frase a ser copiada e pode ser um array, um ponteiro ou um string literal
 char* target = "Jeff Kent";
 char src[80] = "Micaela";
 strcpy(target, src);

Note que esta operação é muito arriscada visto que, quando criamos target, a quantidade de caracteres que foi reservada para a string era de 9 caracteres mais o caractere nulo no final, se fizermos uma cópia de uma string com mais de 9 caracteres para este endereço, representado por target, ele fatalmente causará uma violação de endereço.

Porém em C++ podemos atribuir o valor de uma variável para outra da classe string da forma:

   string target = "Jeff Kent";
   string src = "Micaela";
   target = src;

Agora, reflitamos no que significa estas operações: Em primeiro lugar "string" não é um tipo primitivo de dado, é uma classe, portanto é um tipo de dado mais "inteligente", uma das características dos objetos string é que eles são redimensionáveis, ou seja, quando atribuímos a uma string um dado maior que seu espaço interno de armazenamento ela aumenta o seu espaço interno para comportar o novo dado. Outra característica é que a operação "=" para a string é uma operação de atribuição de conteúdo, de forma que a string copia a outra quando usamos este operador e não apenas o ponteiro que referência o endereço da string.

Unir strings

editar

strcat() – (string+concatenate) – une duas frases. Recebe 2 argumentos, a frase primária – o ponteiro para esse array.

   char target[80]  = "Jeff";
   char* source= " Kent";
   strcat(target, source);
   cout << target;    // Mostra "Jeff Kent"

Deve-se observar que strcat é, potencialmente, uma das rotinas mais perigosas do C, por um motivo bem simples: a string de destino deve ser pre-dimensionada, e deve ter espaço suficiente para receber a string de origem. Um pequeno programa como:

   char target[13] = "Regras do C!";
   char* source = " Mas pode dar resultados imprevisiveis";
   strcat(target, source);

Escreverá bytes em regiões da memória que não foram previamente alocadas para a string. Em c++, este problema é resolvido pelo uso de objetos string.

Ao estilo de c++ podemos fazer.

   string target =  "Regras do C++!\n";
   string source = " Geralmente não dão resultados imprevisiveis.\n";
   target += source;
   cout << target;    // Mostra: Regras do C++!
                      //         Geralmente não dão resultados imprevisiveis.

Isto porque a classe string prevê o uso do operador "+=" de concatenação e nele está embutido um código de verificação de espaço e realocação do mesmo para string, caso seja necessário.

comparar frases

editar

se fizessemos

   char str1[80] = "Devvie Kent";
   char str2[80] = "Devvie Kent";
   if (str1 == str2)
      cout << "The two C-strings are equal";
   else
      cout << "The two C-strings are not equal";

o que acontecia é que estariamos a comparar os endereços e não os valores

temos a função strcmp (string+compare) (tem 2 arguentos. retornar 0 se forem iguais)

   char str1[80] = "Devvie Kent";
   char str2[80] = "Devvie Kent";
   if (!strcmp(str1, str2))
     cout << "The two C-strings are equal";
   else
      cout << "The two C-strings are not equal";

esta comparação pode ser resultar em negativo e positivo e isso tem a ver com o jogo de caracteres na tabela ascII. aqui vai um resumo

Resultados de comparações entre strings

Primeira string (str1) Segunda string (str2) strcmp(str1, str2) Razão
Jeff jeff negativo j tem valor ASCII maior que J
aZZZ Zaaa positivo a tem valor ASCII maior que Z
chess check positivo Os primeiros três caracteres são os mesmos, mas a quarta letra da primeira string-C, s, tem maior valor ASCII que a quarta letra da segunda string-C, c.
Jeff Jeffrey negativo Os quatro primeiros caracteres são os mesmos, mas o quinto caractere da segunda string-C, r, tem valor ASCII maior que o caractere nulo na quinta posição da primeira string-C.

Em C++ podemos comparar duas strings através da função membro da classe string: compare(), existem os seguintes formatos (assinaturas) para a função:

1 int compare ( const string& str2 ) const;
2 int compare ( const char* szc ) const;
3 int compare ( size_t pos1, size_t n1, const string& str2 ) const;
4 int compare ( size_t pos1, size_t n1, const char* szc) const;
5 int compare ( size_t pos1, size_t n1, const string& str2, size_t pos2, size_t n2 ) const;
6 int compare ( size_t pos1, size_t n1, const char* szc, size_t n2) const;

A função permite os seguintes modos de operação, respectivamente:

  1. Comparar uma "string" de entrada (str2) com o conteúdo do objeto a qual ela pertence;
  2. Comparar uma "C-string" apontada por um ponteiro com o conteúdo do objeto a qual ela pertence;
  3. Comparar uma seção começando em (pos1) do objeto, a qual contém (n1) caracteres, com a "string" de entrada (str2);
  4. Comparar uma "C-string" apontada por um ponteiro (szc), com uma seção começando em (pos1), a qual contém (n1) caracteres do conteúdo do objeto a qual ela pertence;
  5. Comparar uma seção do objeto, iniciada em (pos1) com (n1) caracteres, com uma seção de (str2), iniciada em (pos2) com (n2) caracteres;
  6. Comparar uma "C-string" apontada por um ponteiro (szc) de extensão (n2), com uma seção começando em (pos1), a qual contém (n1) caracteres do conteúdo do objeto a qual ela pertence.

O resultado é similar ao da função strcmp() em "C", retornando uma referência de valor de acordo com o código ASCII.

Se estiver comparando duas strings uma outra opção, ainda mais natural, é utilizar os operadores de comparação < e ==.

using namespace std;

string str1 = "check";
string str2 = "chess";
if (str1 == str2)
    cout << "As palavras são iguais." << endl; 
else if (str1 < str2) 
   cout << "A palavra " << str1 << " vem antes de " << str2 << endl;
else
   cout << "A palavra " << str1 << " vem depois de " << str2 << endl;

Convertendo C-string e número

editar

No ficheiro (arquivo) cabeçalho da biblioteca cstdlib (c+std+lib) temos várias funções de conversão de números em tipo numérico.

atoi (acrônimo para "ASCII to integer") recebe um argumento – c-string) e retorna o inteiro que a c-string representa. Não verifica se o argumento pode ser convertido:

 int num = atoi("7654");

Programa exemplo:

 #include <iostream>
 #include <cstdlib> // necessário para atoi
 #include <cstring>

 using namespace std;

 int main(void)
 {
   char input[80];
   int num;
   cout << "Enter an integer: ";
   cin >> input;
   for (int x = 0; x < strlen(input); x++)
   {
      if (x == 0)
      {
         if (!isdigit(input[x]) && input[x] != '-')
            return 1;
      }
      else 
      {
         if (!isdigit(input[x]))
            return 2;
      }
   }
   num = atoi(input);
   cout << num;

#ifdef WIN32
   system ("pause");
#endif

   return 0;
 }

Neste exemplo temos a vantagem de o usuário inserir um dígito para o array de caracteres em vez de um inteiro, para evitar um "run-time error" ou "garbage data" que aconteceria se a entrada fosse não numérica. Depois o array é verificado para ver se representa um número. Se o numero for negativo tem o caractere "–".


Em C++ usamos objetos da classe stringstream (biblioteca sstream.h) para armazenar temporariamente os caracteres, depois usamos o operador ">>" para converter os caracteres em número, bastando para isto criar a variável no formato que desejamos receber o número. Mais uma vez temos o uso do poliformismo para resolução de um problema comum de programação, a operação do ">>" é diferente para cada tipo de dado, selecionada automaticamente pelo compilador de acordo com o tipo de dado da variável destino.

 string name = "123";
 stringstream sst;
 int i;
 sst << name << endl;
 sst >> i;

Os passos acima armazenam o valor 123 na variável "i", todo processo de conversão é feito pelo operador ">>".


  Esta página precisa ser reciclada (discuta).
Ao melhorá-la, você estará ajudando o Wikilivros.

Classes

editar

Existem duas categorias de tipos de dados usuais em C++, são classificados como tipos básicos e tipos definidos pelo programador.

Assim como na linguagem C, podemos definir dados compostos por associações dos tipos básicos, estes tipos são chamados de estruturas (structs). C++ traz uma nova representação de dados, muito semelhante na forma às estruturas, porém diferentes na forma conceitual: a palavra chave class, que é usada para criar uma classe de objetos mais rica que as structs. Ao declararmos um identificador, tal qual fazemos ao declarar uma variável e no lugar do tipo especifiquemos uma classe criaremos um objeto.

Antes de prosseguirmos, vejamos um pouco sobre o conceito por trás do uso de objetos. Um objeto é entendido como uma entidade de dados dentro da memória que, basicamente, deve ser responsável por seu conteúdo, ou seja, um objeto deve ser capaz de gerenciar seu conteúdo autonomamente, ou prover meios de outras entidades de código fazê-lo de forma segura.

Origem (atributos)

editar

Observemos, por exemplo, o código abaixo:

struct MyData
{ int    n;
  char   data[10];
  float  nReal;
};

Esta declaração, bem conhecida de quem já está familiarizado com a linguagem C, cria um tipo de dado composto heterogêneo, que neste exemplo chamamos de MyData, o que acontece aqui é que os dados estão agrupados dentro desta estrutura, isto promove a possibilidade de manipulá-los em conjunto. Um dos problemas com esta estrutura é a presença de uma matriz de caracteres chamada "data", observe que a mesma tem um tamanho definido de 10 caracteres, imagine que em algum momento da execução do programa tentamos colocar um caractere na posição 11, ou qualquer posição fora da matriz, neste caso estamos colocando o referido dado em endereços inválidos para a operação que pretendemos realizar, ou seja, não há controle nenhum que assegure que o código não fará um acesso fora da área que pertença a matriz. Um acesso de memória a qualquer elemento da matriz acima da posição 9, fará com que invadamos dados na área onde a variável nReal está definida.

Funções membro (Métodos)

editar

Agora suponha que tenhamos como definir um modo para entrar e outro para ler dados da matriz:

struct MyData
{ int    n;
  char   data[10];
  float  nReal;

  bool write_data(int pos, char c)
    { if (pos >= 0 && pos < 10) 
          {   data[pos]=c;
              return true;
          }
      return false;
    }

 char read_data(int pos)
    { if (pos >= 0 && pos < 10) 
          {  return data[pos];
          }
      return '\0';
    }
};

Agora temos assegurados métodos de inclusão e acesso a dados da matriz de caracteres, porém ainda existe um pequeno problema: Quem quiser o antigo método de acesso direto conseguirá facilmente, pois os elementos da estrutura estão acessíveis publicamente por padrão.

Conceituação

editar

O problema da visibilidade pública dos dados em uma estrutura pode ser resolvido com um dos conceitos de objetos, o encapsulamento. Encapsular os dados, significa reservar o acesso a funções que estejam dentro de um grupo restrito, especializado para tais operações de manipulação destes dados. Uma das vantagens deste procedimento é que o código adquire um formato mais organizado, onde os processos tornam-se claramente distintos, caso tenhamos que analisar o código, cada procedimento estará restrito a partes definidas para cada operação.

Declarando classes

editar

As estruturas são bem parecidas com as classes, com uma pequena diferença, peguemos o caso da passagem de estruturas como argumentos de funções:

#include <iostream>
#include <string>

 using namespace std;

 class Person 
 {
   string name; 
   int height;
 };

 void setValues(Person&);
 void getValues(const Person&);

 int main ()
 {
   Person p1;
   setValues(p1);  
   cout << "Informando dados sobre a pessoa:\n";
   cout << "================================\n";
   getValues(p1);
   return 0;
 }

 void setValues(Person& pers)
 {
   cout << "Informe o nome da pessoa: ";
   getline(cin, pers.name);
   cout << "Informe a altura em milímetros: ";
   cin >> pers.height; 
   cin.ignore();
 }

 void getValues(const Person& pers)
 {
   cout << "Nome da pessoa: " << pers.name << endl; 
   cout << "A altura da pessoa em milímetros é: " << pers.height << endl;
 }


  • Mudamos o identificador de struct para class
  • Mas se tentarmos compilar o programa isto vai causar erros de compilação, porque agora temos variáveis membro que são privadas por padrão, estas não são vistas por funções fora da classe.

Dentro de uma classe podemos definir diversos modos de visibilidade de variáveis e funções.

As modalidades podem ser:

  • private (só podem ser acessados por membros da mesma classe)
  • public (pode ser acessadas fora do objeto, onde este estiver definido)
  • protected (deixemos esta para quando falarmos em classes derivadas, pois depende deste conceito).

Ora, como as funções getValues e setValues não são membros da classe Person, tal como o construtor Person, não conseguem acessar as variáveis "name" e "height".

Visualizamos melhor em forma de tabela:

Class Person private
string name
Int height

p1

A solução é criar funções publicas, para ler de e escrever para as variáveis privadas:

 #include <iostream>
 #include <string>
 
 using namespace std;

 class Person 
   {
   private:
      string name; 
      int height;      
   public:
      string getName() const;
      void setName(string);
      int getHeight() const;
      void setHeight(int);
   };

   string Person::getName() const
   { return name; }

   void Person::setName(string s)
   { 
      if (s.length() == 0)
         name = "No name assigned";
      else
         name = s; 
   }

   int Person::getHeight() const
   { return height; }

   void Person::setHeight(int h)
   { 
      if (h < 0)
         height = 0;
      else
         height = h; 
   }

 void setValues(Person&);
 void getValues(const Person&);

 int main ()
 {
   Person p1;
   setValues(p1);  
   cout << "Outputting person data\n";
   cout << "======================\n";
   getValues(p1);
   return 0;
 }

 void setValues(Person& pers)
 {
   string str;
   int h;
   cout << "Enter person's name: ";
   getline(cin,str);
   pers.setName(str);
   cout << "Enter height in milimeters: ";
   cin >> h;
   cin.ignore();
   pers.setHeight(h);
 }

 void getValues(const Person& pers)
 {
   cout << "Person's name: " << pers.getName() << endl; 
   cout << "Person's height in milimeters is: " << pers.getHeight() << endl;  
 }

Mas perguntam: Por que é que nos demos ao trabalho de recorrer a membros privados em vez de fazer todos públicos? Quando tínhamos uma estrutura no lugar de uma classe, não havia nada que impedisse a colocação de valores inválidos, por isso poderíamos ter valores vazios para a string e valores negativos para a variável "height".

Agora que "Person" é uma classe, as funções membro podem realizar a validação dos dados antes da atribuição de valores nas variáveis. Poderíamos fazer com que a função setName verificasse se a entrada na string seria vazia e caso fosse, colocaria um valor padrão como: “sem nome”. similarmente poderíamos ter "setHeight" para verificar se seriam colocados valores de entrada negativos e caso fossem, colocaria zero, ou não tomaria nenhuma ação.

Todas estas características demonstram o conceito de encapsulamento. A sua finalidade é de tornar o código mais modularizado, restringindo o escopo de análise a partes bem delimitadas dos programas. Devido a este conceito podemos contar com códigos mais fáceis de analisar e fazer manutenção.

Instanciando objetos

editar

Instanciação de objetos é o processo de criar a estrutura lógica dos mesmos na memória. Isto ocorre quando declaramos os objetos, pois neste momento todo o processo de construção dos mesmos é efetivado. Assim, toda vez que declaramos um objeto estamos instanciando-o, ou seja, estamos criando uma instância da classe.

Podemos declarar os objetos logo após definir a classe conforme podemos ver no 1º caso logo abaixo. Neste caso teremos a variável rect criada como um objeto conforme estabelecido pelo modelo definido pela palavra chave class. Este tipo de declaração é mais usual para objetos criados globalmente, pois a inclusão desta declaração no cabeçalho pode fazer com que vários objetos sejam criados com o mesmo nome quando o cabeçalho é invocado de vários arquivos. Portanto, é mais prudente usar esta opção quando a declaração está no arquivo fonte e não no cabeçalho.


1º caso:

 class CRectangle 
 {
   int x, y;
  public:
    void set_values (int,int);
    int area (void);
  } rect;

No 2º caso, apresentado logo abaixo, podemos declarar objetos apenas quando precisarmos. Esta opção de declarar o objeto depois é a mais usada, pois na maioria das vezes temos o modelo dos objetos, a classe, declarada em um arquivo de cabeçalho enquanto que os objetos serão criados no resto do código fonte. Desta forma é mais usual criar as classes em cabeçalhos e depois declarar os objetos na parte do programa que for mais conveniente.


2º caso:

class CRectangle 
 {
    int x, y;
  public:
    void set_values (int,int);
    int area (void);
  };

 int main()
 {
  CRectangle rect;
 }


Em ambos os casos temos

CRectangle Private public
int x
int y
void set_values (int,int);
int area (void);

rect

Podemos, então, entender os objetos como blocos de dados que têm propriedades (variáveis) e que podem fazer algo (métodos). Então, criamos todas as funcionalidades que precisamos que a classe forneça aos programas, fazendo os testes necessários para assegurar sua consistência e estabilidade. Sempre que precisemos utilizar os objetos só temos que instanciá-los (declará-los), e não precisamos nos preocupar como eles funcionam internamente, uma vez que os desenhamos adequadamente.

Para entendermos melhor este conceito podemos fazer uma analogia. Consideremos um objeto resistência: sabemos que temos de usá-lo e que ela deve ter certas características, então teremos o seu valor em Ohms, sua potência máxima, tolerância, entre outras, e teremos uma função que nos dará a corrente que passa por ela quando lhe aplicamos uma tensão elétrica. Não precisamos saber de que é que ela é feita, ou como estas características internas a faz funcionar, basta-nos receber os resultados.

Vejamos o exemplo:

Agora vamos mostrar que podemos ter funções membro apenas como protótipos e defini-las fora da classe. Para isso usamos o operador de definição de escopo :: que permite definir o local do código onde um identificador existe, no formato: ESCOPO::função ou ESCOPO::dado. De maneira geral, quando declaramos identificadores dentro da classe podemos defini-los no escopo global referenciando estes pelo operador de escopo.

 // classes example
 #include <iostream>
 
 using namespace std;

 class CRectangle 
 {
    int x, y;
  public:
    void set_values (int,int);
    int area () {return (x*y);}
 };

 void CRectangle::set_values (int a, int b) 
 {
  x = a;
  y = b;
 }                 

 //repare no “::” que pemite-nos definir a função membro da classe CRectangle fora da classe

 int main () 
 {
  CRectangle rect;     //definimos objeto de classe
  rect.set_values (3,4);       //objeto-membro
  cout << "area: " << rect.area();
  return 0;
 }	

 // classes example
 #include <iostream>
 
 using namespace std;

 class CRectangle 
 {
    int x, y;
  public:
    void set_values (int a,int b)
    {
     x = a;
     y = b;
    }
    int area () {return (x*y);}
 };

 int main () 
 {
  CRectangle rect;         //definimos objeto de classe
  rect.set_values (3,4);       //objeto-membro
  cout << "area: " << rect.area();
  return 0;
 }
area: 12

O exemplo anterior explora a característica de toda figura geométrica fechada, que possui uma área interna. Observe que este modo de definir a classe coloca o cálculo da área dentro da definição da mesma. Este modo faz com que o código seja apenas um modelo, a função de cálculo da área não será criada se não for usada durante o escrita do resto do programa.


Vejamos outro exemplo:

 class Dog 
 {
 public:
    void setAge(int age);
    int getAge();
    void setWeight(int weight);
    int getWeight();
    void speak();
 private:
    int age;
    int weight;
 };
 void Dog::setAge(int age)
 {
    this->age = age;
 }
 int Dog::getAge()
 {
    return age;
 }
 void Dog::setWeight(int weight)
 {
    this->weight = weight;
 }
 int Dog::getWeight()
 {
    return weight;
 }
 void Dog::speak()
 {
    cout << "BARK!!" << endl;
 }

Acima podemos ver um modo de declarar as funções apenas como protótipos, que ficam dentro da declaração da classe. Nesse contexto as funções são definidas fora da classe, usando-se o operador "::" para ligar a função à classe. Neste caso teremos as funções definidas e construídas no código das mesmas, enquanto que o modelo da classe poderá permanecer em um arquivo cabeçalho, o que possibilita incluí-lo em qualquer arquivo de códigos fontes do programa.

Definição de classes

editar

Usa-se a palavra "class" para criar uma classe, seguindo-se depois o nome que se queira dar-lhe e finalmente a definição da mesma entre chaves.

A definição contém:

  • os dados ( propriedades );
  • os métodos (as funções membro)

Vamos acompanhar com um exemplo: Vamos fazer o desenho de uma classe chamada “Image”, que será usada para guardar e manipular uma imagem.

Primeiro perguntamos o que é necessário para guardar uma imagem, depois que tipo de manipulações necessitamos.

A imagem possui 400 pixels de largura e 300 pixels altura. Cada pixel tem as propriedades de cor e imagem. A cor é composta por: vermelho, azul e verde, numa escala de 0 a  . Portanto vamos necessitar de membros para guardar estas informações

Agora planejemos os métodos. Vamos, primeiramente, assumir que temos a restrição de <= 400 pixeis, e estes valores serão feitos pelo construtor na criação do objeto. Nós não precisamos dos métodos para estipular a altura e largura, mas vamos precisar para obter e ler os valores. Esta estratégia também nos ajudará a manter os valores de um determinado pixel e a sua localização.

A primeira versão então seria:

 class Image {
 public:
    int getWidth();
    int getHeight();
    void setX(int x);
    int getX();
    void setY(int y);
    int getY();
    void setRed(double red);
    double getRed();
    void setBlue(double blue);
    double getBlue();
    void setGreen(double green);
    double getGreen();
 private:
    int _width;
    int _height;
    int _x;
    int _y;
    double _red[400][400];
    double _blue[400][400];
    double _green[400][400];
    boolean isWithinSize(int s);
    double clipIntensity(double brightness);
 };

Especificadores de acesso

editar
  • Temos as palavras reservadas private e public – são os chamados especificadores de acesso.
    • private – Especifica uma faixa de variáveis ou funções que podem ser acessadas exclusivamente por membros da classe, de forma que nenhum outro código fora da mesma possa acessá-las;
    • public – Especifica uma faixa de variáveis ou funções que podem ser acessadas por qualquer código no programa, sendo que para as funções internas da classe não é necessário especificar o objeto enquanto que para as outras partes do programa é necessário especificar o objeto a qual estas pertencem.

Esta característica de limitar o acesso e manipulação dos membros de uma classe chama-se Encapsulamento. A boa prática no desenho de classes deve sempre forçar o encapsulamento. É raramente necessário ou desejável ter acesso livre e público aos dados internos de uma classe.

O encapsulamento visa, primariamente duas metas:

  1. Eliminar a necessidade de conhecimento da estrutura interna por quem deseja usá-la. Por exemplo, se os objetos precisam manter um conjunto de 4 bytes, isto pode ser conseguido usando-se duas variáveis short int, uma int, um vetor com 4 characteres, ou a variação de qualquer um dos anteriores sem sinal, mas estes detalhes não precisam estar expostos.
  2. Se a representação interna dos dados for modificada, desde que os tipos de retorno e de parâmetros das funções públicas mantenham-se inalteradas, não necessitemos de alterar código que utilizem objetos da classe.

Ou seja, o encapsulamento simplifica a programação escondendo as particulariadades da classe e elimina o retrabalho do código por alterações da mesma. Geralmente as funções (métodos) privadas, são auxiliares a outras funções da classe.

Se nenhum especificador de acesso for usado, todos os membros e metodos são declarados como privados por padrão.


Há dois métodos para definir as funções membro:

  • Eles podem ser definidos dentro da classe, o que é apropriado para funções pequenas;
  • E funções grandes podem ser definidas fora da classe.

Neste caso terão de ser identificadas como pertencentes à classe e para isso utilizamos o operador de resolução de escopo “::”.

Construtores

editar

Conceito

editar

Os construtores "constructors" são funções membro (métodos) especiais de uma classe. Permitem a inicialização das variáveis membro de um objeto. Ou melhor, permitem a construção e a inicialização de objetos das classes. Se não os declararmos o compilador faz isso por nós. Os construtores têm sempre o mesmo nome que a classe.

Os objetos são construídos através destas funções especiais chamadas de construtores. Até aqui não os declaramos, eram criados automaticamente. Estas funções tem certas características que as fazem distintas das normais, que permitem que as mesmas construam a estrutura lógica inicial do objeto. Desta forma estas funções são características da orientação a objetos e servem para criação dos mesmos.

Construtores não podem ser chamados explicitamente como fazemos no caso de funções membro regulares. Eles são apenas executados quando um novo objeto da classe é criado. Portanto, existe apenas um evento capaz de executar um construtor, a instanciação do objeto.

As principais características dos construtores são:

  • Não têm qualquer valor de retorno;
  • Não podem ser executados por chamada explícita no código;
  • São executados logo depois que os tipos básicos do objeto foram criados;
  • Inicializam os dados com os valores que o objeto precisa para começar a funcionar corretamente.

Declaração

editar

Podemos criar construtores facilmente, através das características que os distinguem das funções membro convencionais. Ou seja, definimos uma função membro que possua o mesmo nome da classe, não tenha tipo de retorno e a declaramos como pública para que possa ser acessada por quem queira instanciar objetos. Vejamos como definir um construtor:

class Caneta
{ string cor;
  int volume;
  ///////////////
  public:
    Caneta( string c, int v );
};

Caneta::Caneta( string c, int v )
{ cor = c;
  volume = v;
}

Construtores podem iniciar os membros da classe de uma forma simplificada. Este formato é usado sempre que o construtor tem dados básicos que podem ser iniciados antes do resto da construção da estrutura do objeto. Podemos iniciar os dados do objeto declarando-o desta forma:

class Caneta
{ string cor;
  int volume;
  ///////////////
  public:
    Caneta( string c, int v ) : cor(c), volume(v)
         {
         }
};

Para fazê-lo, como vemos no código, basta listar as variáveis membro em uma sequência depois da declaração do nome do construtor e de um sinal de dois pontos ":". Iniciamos uma lista de membros, com o valor a ser atribuído entre parênteses depois de cada um, separando-os por vírgulas.

Destrutores

editar

Conceito

editar

Além do construtor a linguagem C++, assim como outras linguagens orientadas a objeto, possuem outro tipo de função especialmente criada e gerenciada pela linguagem, os destrutores. Estas são destinadas a desmontar a estrutura do objeto quando o mesmo está sendo encerrado. O destrutor terá o mesmo nome da classe, mas precedido pelo sinal til “~” e também não retorna valor.

O destrutor tem as seguintes características:

  • O destrutor é chamado quando o objeto está sendo finalizado;
  • É usado para liberar qualquer memória que tenha sido alocada;

Declaração

editar

Façamos a classe Dog com o construtor e o destrutor.

 class Dog 
 {
 public:
    Dog();      //Constructor
    ~Dog();    //Destructor
    void setAge(int age);
    int getAge();
    void setWeight(int weight);
    int getWeight();
    void speak();
 private:
    int age;
    int weight;
 };

  Dog::Dog()		
 {
    age = 0;
    weight = 0;
    cout << "Dog Constructor Called" << endl;
 }

 Dog::~Dog()
 {
    cout << "Dog Destructor Called" << endl;
 }

Repare que:

  • O construtor tem o mesmo nome que a classe;
  • O destrutor tem o mesmo nome que a classe com o prefixo de tilde” ~”;
  • O construtor foi usado para inicializar as variáveis membro, mas noutros exemplos poderia alocar memória, tomar controle de recursos como dispositivos de sistema e executar inicializações de código;
  • O destrutor no exemplo não faz nenhuma ação real, para além de fazer o eco informando que foi chamado.

"copy constructors"

editar

Um "copy constructor" é um construtor especial que toma como argumento a referência de um objeto da mesma classe e cria um novo objeto que é a copia do objeto em referência. Por padrão, o compilador providencia um "copy constructor" que faz a cópia membro por membro do objeto original, construindo um objeto idêntico. Isto é chamado de "shallow copy" ou "member wise".

Em algumas situações a cópia de um objeto não é satisfatória, para ver isso vamos ver a classe employee, abaixo:

 #include <iostream>
 #include <string.h>
 
 using namespace std;

 class Employee 
 { 
   public:
     Employee(char *name, int id);
     ~Employee();
     char *getName(){return _name;}
   private://Other Accessor methods
     int _id;
     char *_name;
 };

 Employee::Employee(char *name, int id)
 {
    _id = id;
    _name = new char[strlen(name) + 1];        //Allocates an character array object
    strcpy(_name, name);
 }

 Employee::~Employee()
 {
    delete _name;
 }

 int main()
 {
    Employee programmer("John",22);
    cout << programmer.getName() << endl;
    return 0;
 }

A função strlen retorna o tamanho da string passada pelo constructor. Repare que o nome do employee é agora guardado num carácter array dinâmico. é o string lenght +1 para permitir o null terminator usado no estilo c.

A função strcpy automaticamente adiciona o null terminator a string destino.

Note também que o destrutor liberta a memoria usada para guardar o employee name, para evitar memory leak.

Agora imagine que o john é promovido:

 int main()
 {
    Employee programmer("John",22);
    cout << programmer.getName() << endl;

    //Lots of code ....

    Employee manager(&programmer);
        //Creates a new Employee "manager",
        //which is an exact copy of the 
        //Employee "programmer".

    return 0;
 }

Este programa contém um bug sério e morre com uma exceção quando é executado. O problema é que o construtor que está sendo usado é para criar um objeto “manager", mas ele copia o endereço no ponteiro _name em "manager".

Nos temos 2 pointers ambos contendo o mesmo endereço. Imagine que agora um novo empregado é contratado. quando o nome for atualizado, não apenas iremos alterar o nome do empregado mas também do gerente. Finalmente, quando os objetos deixarem de ser usados e o destrutor da classe fizer a liberação de espaço na memória tentará liberar duas vezes para o mesmo endereço, provocando um erro no sistema de alocação dinâmica de memória, o que forçará o sistema operacional a eliminar o programa da memória.

Para resolver esse problema podemos definir um construtor de cópia ("copy constructor") na classe, substituindo a implementação padrão do compilador. Este recurso é automaticamente identificado e faz com que o compilador não crie a sua versão do construtor. Assim, definir um construtor de cópia próprio é a maneira mais eficiente quando nossos objetos detêm características que os fazem diferentes do padrão.

Para criar o construtor basta definí-lo na classe:

 class Employee 
 { 
   public:
     Employee( const Employee & e);
     Employee(char *name, int id);
     ~Employee();
     char *getName(){return _name;}
   private://Other Accessor methods
     int _id;
     char *_name;
 };

Employee::Employee( const Employee & e )
 {
    _id = e._id;
    _name = new char[strlen(e._name) + 1];        //Allocates an character array object
    strcpy(_name, e._name);
 }

Agora temos um construtor que pode fazer a cópia da forma correta. Neste novo construtor alocamos uma cadeia de caracteres para copiar o conteúdo da original no objeto a ser copiado e copiamos o conteúdo para o novo objeto. Assim, teremos objetos distintos, cada um com seu próprio conteúdo.

Ver também

editar

Conceito

editar

Encapsulamento, em linguagens orientadas a objeto, é a capacidade de ocultação de detalhes de implementação por parte de entidades de manipulação de dados. Esta característica visa prover um meio de manter cada classe responsável por operações a elas atribuídas sem interferências externas. A vantagem dessa característica é de manter os indivíduos de cada classe com funções bem delimitadas e criar meios de criar módulos onde cada classe faça bem aquilo de que está encarregada, tendo total controle sobre tais operações.

Atributos de restrição

editar

Classes podem proteger sua estrutura de acessos de outras entidades de dados que não pertencem a seu corpo. Para isto, em C++ temos atributos de acesso para blocos de dados ou funções membros de classes. Em algumas linguagens os atributos são definidos para cada membro individualmente.

Os atributos de restrição de acesso em C++ são três: private, public e protected. Cada atributo oferece um nível de ocultação para membros de classes. Eles são usados para assegurar que apenas grupos identificados de código tenham acesso a partes presselecionadas da classe.

Os níveis de proteção estão ligados ao parentesco do código que pretende acesso com a classe em que os mesmos estão definidos. Mais especificamente, classes que não são filhas da que pretendem acessar só poderão ter acesso a membros públicos, classes filhas terão acesso a membros protegidos (protected) ou públicos (public) e finalmente, nenhum código que não pertença a própria classe poderá acessar membros privados (private).

Classes derivadas (pequena introdução)

editar

Precisamos dos conceitos básicos de herança para entender alguns conceitos de encapsulamento. Para não criarmos dependências circulares entre os tópicos, ou seja, para não dependermos de conceitos de herança que também precisa de tópicos de encapsulamento, faremos uma pequena introdução dos conceitos de classes derivadas antes de prosseguirmos com o nosso estudo de encapsulamento.

Uma classe pode ser estendida a partir de outra, ou seja, podemos reaproveitar um código já existente em uma determinada classe que já temos e criar uma nova classe com tudo que já existia na primeira, mais o que definirmos para a nova.

Vejamos um exemplo básico:

class veículo
{ 
  string cor;
  string combustivel;
  ...
  ...
};

class carro : public veiculo
{
 int nrodas;
 ...
 ...
 int mover( int nkilometros );

};

A segunda classe declarada possui a extensão ": public veiculo" a mais, esta parte refere-se a uma declaração de parentesco. De fato, ao declarar a classe desta forma estamos informando ao compilador que a classe veiculo é mãe da classe carro. Semanticamente, isto significa que a classe carro possui toda a estrutura da classe veiculo além de seus próprios membros.

Definindo acessos

editar

Considerando o exemplo anterior, podemos observar que os atributos "cor", "combustivel", "nrodas" poderiam ser alterados em qualquer ponto do programa se tivéssemos usado a palavra struct, porém usando a palavra class algo de diferente ocorre, pois não podemos ter acesso a estes atributos, a menos que estejamos acessando-os através de funções definidas dentro das classes.

Em classes, o atributo private é definido por padrão, ou seja, os membros que não tenham definidos os seus atributos de acesso explicitamente, serão definidos como privados. Este comportamento revela a necessidade de resguardar os membros de uma classe através de atributos de restrições. Em C++, ao definir membros em uma classe antes da definição de qualquer atributo de restrição estamos definindo-os como privados (private).

Ainda levando em consideração o exemplo anterior, podemos definir atributos de restrições para grupos de membros e modificar o comportamento padrão da classe. Se definirmos public, os dados estarão acessíveis a qualquer parte do programa, o que é equivalente a coloca-los em uma estrutura com a palavra struct ao invés de dentro de uma classe. Se definirmos protected temos uma situação peculiar, apenas funções membro da classe ou de suas "filhas" poderão acessar dados da classe mãe.

Vejamos o exemplo anterior com algumas alterações:

class veiculo
{ 
  string cor;

  protected:
    string combustivel;
  ...
  ...
 public:
  bool altCor( string c )
       { if ( c == "vermelho" )
           { cor = c;
             return true;
           } 

         if ( c == "azul" )
           { cor = c;
             return true;
           } 

         if ( c == "prata" )
           { cor = c;
             return true;
           }

        return false; 
       }
};

class carro : public veiculo
{
 int nrodas;
 ...
 ...
 int mover( int nkilometros );

};

Neste exemplo definimos que o atributo cor não pode ser modificado, a não ser pela função altCor(), onde restringimos as cores a um conjunto que desejamos. Observe que ao tentar atribuir qualquer cor diferente de "vermelho", "azul" e "prata", receberemos um retorno "false". Assim, temos a possibilidade de controlar o comportamento do objeto criado através da restrição imposta.

O atributo "combustível" será manipulado livremente pela classe "veículo" e pela classe "carro", visto que o atributo protected dá visibilidade às classes derivadas de "veiculo". O mesmo mecanismo de controle visto no parágrafo anterior poderá ser implementado para acessos fora do escopo das duas classes, ou seja, para funções no escopo global ou em outras classes.

Escopos globais

editar

Até aqui vimos os atributos de restrição sendo usados em classes para encapsular partes internas a elas, porém, existe outro mecanismo de encapsulamento muito útil em C++, os namespaces. Estes "espaços de nomes" são meios de delimitar áreas onde símbolos são usados, o que permite evitar que erros ocorram por coincidência de nomes.

A sintaxe para criação de um namespace é bem simples. Vejamos um exemplo de código, para observarmos os detalhes:

 namespace MeuEspaco
 { void print()
   { cout << "Função de imprimir no meu espaco" << endl;
   }
 }

 namespace EspacoAlheio
 {
  void print()
   { cout << "Função de imprimir no Espaco alheio" << endl;
   }
 }


Conceito

editar

Herança é um dos pontos chave de programação orientada a objetos (POO). Ela fornece meios de promover a extensibilidade do código, a reutilização e uma maior coerência lógica no modelo de implementação. Estas características nos possibilitam diversas vantagens, principalmente quando o mantemos bibliotecas para uso futuro de determinados recursos que usamos com muita frequência.

Uma classe de objetos "veiculo", por exemplo, contém todas as características inerentes aos veículos, como: combustível, autonomia, velocidade máxima, etc. Agora podemos dizer que "carro" é uma classe que têm as características básicas da classe "veículo" mais as suas características particulares. Analisando esse fato, podemos concluir que poderíamos apenas definir em "carro" suas características e usar "veículo" de alguma forma que pudéssemos lidar com as características básicas. Este meio chama-se herança.

Agora podemos definir outros tipos de veículos como: moto, caminhão, trator, helicóptero, etc, sem ter que reescrever a parte que está na classe "veículo". Para isso define-se a classe "veículo" com suas características e depois cria-se classes específicas para cada veículo em particular, declarando-se o parentesco neste instante.

Outro exemplo: Imagine que já exista uma classe que defina o comportamento de um dado objeto da vida real, por exemplo, animal. Uma vez que eu sei que o leão é um animal, o que se deve fazer é aproveitar a classe animal e fazer com que a classe leão derive (herde) da classe animal as características e comportamentos que a mesma deve apresentar, que são próprios dos indivíduos classificados como animais.

Ou seja, herança acontece quando duas classes são próximas, têm características mútuas mas não são iguais e existe uma especificação de uma delas. Portanto, em vez de escrever todo o código novamente é possível poupar algum tempo e dizer que uma classe herda da outra e depois basta escrever o código para a especificação dos pontos necessários da classe derivada (classe que herdou).

Sintaxe

editar

Para declarar uma classe derivada de outra já existente, procedemos de forma a declarar o parentesco e o grau de visibilidade (acesso) que a classe derivada terá dos membros de sua classe base. Para isso seguimos o seguinte código sintático:

 class classe_derivada : [<acesso>] classe_base {
  //corpo da classe derivada
 }

Repare que temos o operador ":" ( dois pontos ) como elo entre as duas classes. Este operador promove o "parentesco" entre as duas classes quando é usado na declaração de uma classe derivada.

O termo [<acesso>] é opcional, mas se estiver presente deve ser public, private ou protected. Ele define o grau de visibilidade dos membros da classe base quando a classe derivada precisar acessá-los.

Exemplo de implementação:

 // Demonstra herança.
 #include <iostream>
 using namespace std;
 class veiculo_rodoviario // Define uma classe base veículos.
    {
        int rodas;
        int passageiros;
    public:
        void set_rodas(int num) { rodas = num; }
        int get_rodas() { return rodas; }
        void set_pass(int num) { passageiros = num; }
        int get_pass() { return passageiros; }
    };
 class caminhao : public veiculo_rodoviario // Define um caminhao.
    {
          int carga;
    public:
           void set_carga(int size) { carga = size; }
           int get_carga() { return carga; }
           void mostrar();
    };
 enum tipo {car, van, vagao};
 class automovel : public veiculo_rodoviario // Define um automovel.
    {
        enum tipo car_tipo;
    public:
        void set_tipo(tipo t) { car_tipo = t; }
        enum tipo get_tipo() { return car_tipo; }
        void mostrar();
    };
 void caminhao::mostrar()
        {
        cout << "rodas: " << get_rodas() << "\n";
        cout << "passageiros: " << get_pass() << "\n";
        cout << "carga (capacidade em litros): " << carga << "\n";
        }
 void automovel::mostrar()
        {
        cout << "rodas: " << get_rodas() << "\n";
        cout << "passageiros: " << get_pass() << "\n";
        cout << "tipo: ";
        switch(get_tipo()) 
                {
                case van: cout << "van\n";
                          break;
                case car: cout << "carro\n";
                          break;
                case vagao: cout << "vagao\n";
                }
        }
 int main()
 {
    caminhao t1, t2;
    automovel c;
    t1.set_rodas(18);
    t1.set_pass(2);
    t1.set_carga(3200);
    t2.set_rodas(6);
    t2.set_pass(3);
    t2.set_carga(1200);
    t1.mostrar();
    cout << "\n";
    t2.mostrar();
    cout << "\n";
    c.set_rodas(4);
    c.set_pass(6);
    c.set_tipo(van);
    c.mostrar();
#ifdef WIN32
    system ("pause");
#endif
    return 0;
 }

Na implementação acima temos a classe base veiculo_rodoviario e duas classes derivadas “:” caminhao e automovel. Podemos notar que as características comuns a todos os tipos de veículos, rodas e passageiros, estão na classe base, enquanto as características exclusivas de cada tipo de veículo estão nas classes derivadas. Desta forma podemos definir procedimentos especializados para cada classe, fazendo com que todas as eventuais modificações feitas ao longo da implementação na classe base sejam estendidas a todos os objetos criados a partir das classes derivadas no programa.


Repare ainda um pormenor: tanto a classe "caminhao" quanto a automovel têm como função membro o método mostrar(), mas uma não interfere com a outra. Isto ilustra um outro aspecto da orientação a objeto, o polimorfismo. Este será exposto em mais detalhe nos capítulos subsequentes.

Controle de acesso à classe base

editar

Quando uma classe herda outra, os membros da classe base são incorporados como membros da classe derivada. Devido à separação das classes e do controle de acesso às variáveis em cada classe, devemos pensar como as restrições de acesso são gerenciadas em classes diferentes, principalmente o acesso a membros da classe base a partir das classes derivadas.

O acesso dos membros da classe base à classe derivada é determinado pelo especificador de acesso: public, private e protected. Por "defaut" (padrão) temos o private, ou seja, como temos a opção de não explicitar o especificador de acesso, se este não estiver presente o compilador usará "private" durante a interpretação do código.

Assim ficamos com as possíveis combinações


  • Classe base herdada como public:


    • Membros públicos (public) da classe base:
      • É como se copiássemos os membros da classe base e os colocássemos como "public" na classe derivada. No final, eles permanecem como públicos.

    • Membros privados (private) da classe base:
      • Os membros estão presentes na classe, porém ocultos como privados. Desta forma as informações estão presentes, mas só podem ser acessadas através de funções publicas ou protegidas da classe base.

    • Membros protegidos (protected) da classe base:
      • Se tivermos membros protegidos (protected) na classe derivada, eles se comportam como se tivessem sido copiados para a classe derivada como protegidos (protected).



  • Classe base herdada como private:


    • Membros públicos (public) da classe base:
      • Os membros se comportam como se tivessem sido copiados como privados (private) na classe derivada.

    • Membros privados (private) da classe base:
      • Os membros estão presentes na classe, porém ocultos como privados. Desta forma as informações estão presentes, mas não poderão ser acessadas, a não ser por funções da classe base que se utilizem delas.

    • Membros protegidos (protected) da classe base:
      • Os membros se comportam como se tivessem sido copiados como privados (private) na classe derivada.



  • Classe base herdada como Protected:


    • Membros públicos (public) da clase base:
      • Se comportam como se tivéssemos copiado-os como protegidos (protected) na classe derivada

    • Membros privados (private) da classe base:
      • Os membros estão presentes na classe, porém ocultos como privados. Desta forma as informações estão presentes, mas não poderão ser acessadas, a não ser por funções da classe base que se utilizem delas.

    • Membros protegidos (protected) da classe base:
      • Se comportam como se estivéssemos copiado-os como protegidos (protected) na classe derivada.

Em suma, estas regras podem ser sintetizadas em uma regra muito simples: Prevalece o atributo mais restritivo. Para isto basta-nos listar os atributos por ordem de restrições decrescente:

  1. private
  2. protected
  3. public

Assim, temos todas as combinações definidas quando colocamos um atributo combinado com o outro, bastando para isto escolher sempre o mais restritivo na combinação. Por exemplo: Quando temos uma variável pública na base e a herança é privada, a combinação resulta em uma variável privada na classe derivada.

Aqui está um exemplo muito simples:

 #include <iostream>

 using namespace std;

 class base 
 {
      int i, j;
 public:
       void set(int a, int b) { i = a; j = b; }
       void show() { cout << i << " " << j << "\n"; }
 };

 class derived : public base 
 {
      int k;
 public:
       derived(int x) { k = x; }
       void showk() { cout << k << "\n"; }
 };

 int main()
 {
    derived ob(3);
    ob.set(1, 2); // acesso a membro da base
    ob.show(); // acesso a membro da base
    ob.showk(); // uso de membro da classe derivada
#ifdef WIN32
    system ("pause");
#endif
    return 0;
 }

Conseguimos acessar as funções set() e show() porque são heradadas como publicas.

Agora modifiquemos o atributo de acesso na declaração da herança da classe base:

 #include <iostream>

 using namespace std;

 class base 
 {
      int i, j;
 public:
       void set(int a, int b) { i = a; j = b; }
       void show() { cout << i << " " << j << "\n"; }
 };

 class derived : private base 
 {
      int k;
 public:
       derived(int x) { k = x; }
       void showk() { cout << k << "\n"; }
 };

 int main()
 {
    derived ob(3);
    ob.set(1, 2); // Erro, não é possível acessar set()
    ob.show(); // Erro, não é possível acessar show()
    ob.showk(); // uso de membro da classe derivada
#ifdef WIN32
    system ("pause");
#endif
    return 0;
 }

Agora já não podemos acessar as funções porque estão privadas.

Heranças múltiplas

editar

Podemos ter a situação em que uma classe derivada possa herdar membros de várias classes base. Esta característica é uma distinção entre C++ e outras linguagens orientadas a objeto. Este recurso dá mais poder de modelagem ao programador, mas vale a pena lembrar que mais poder exige mais cautela no uso.

 // Um exemplo de múltiplas classes base.
 #include <iostream>

 using namespace std;

 class base1 
    {
    protected:
              int x;
    public:
           void showx() { cout << x << "\n"; }
    };

 class base2 
    {
    protected:
              int y;
    public:
           void showy() { cout << y << "\n"; }
    };

 class derived: public base1, public base2 // Inherit multiple base classes.
    {
    public:
           void set(int i, int j) { x = i; y = j; }
    };

 int main()
 {
    derived ob;
    ob.set(10, 20); // Disponível pela classe "derived"
    ob.showx(); // Pela classe base1
    ob.showy(); // Pela classe base2
#ifndef WIN32
    system ("pause");
#endif
    return 0;
 }

Repare que utilizamos o operador vírgula para dizer ao compilador que a classe derivada herda mais de uma classe. Com efeito, temos uma lista de classes separadas por vírgulas depois do operador ":" (dois pontos).

Quando queremos que a classe derivada herde uma classe como pública e outra como privada ou protegida basta preceder a classe com o seu especificador de acesso. Da mesma forma, a omissão do especificador leva o compilador a usar o padrão que é privado (private).

Construtores e destrutores

editar

Temos uma série de classes que mantém relações de parentesco conforme mostramos nas seções anteriores. Em termos genéricos, classes que herdam características de uma base precisam de regras claras quando forem criadas e destruídas. Precisamos definir a sequência em que os construtores e destrutores serão chamados, uma vez que cada classe tem pelo menos um construtor e um destrutor.

Agora temos a questão: Quando é que os construtores são chamados quando eles são herdados?

  • Quando um objeto da classe derivada é instanciado, o construtor da classe base é chamado primeiro seguido do construtor das classes derivadas, em sequência da base até a última classe derivada.
  • Quando o objeto da classe derivada é destruído, o seu destrutor é chamado primeiro seguido dos destrutores das outras classes derivadas logo abaixo, em sequência até a base.

Vamos testar e ver como isto funciona.

No caso em que termos herança sequencial A-B-C, teremos:

 #include <iostream>

 using namespace std;

 class base 
    {
    public:
           base() { cout << "Construindo base" << endl; }
           ~base() { cout << "Destruindo base" << endl; }
    };

 class derivada1 : public base 
    {
    public:
           derivada1() { cout << "Construindo derivada1" << endl; }
           ~derivada1() { cout << "Destruindo derivada1" << endl; }
    };

 class derivada2: public derivada1 
    {
    public:
           derivada2() { cout << "Construindo derivada2\n"; }
           ~derivada2() { cout << "Destruindo derivada2\n"; }
    };

 int main()
 {
    derivada2 ob; // constrói e destrói o objeto ob
#ifdef WIN32
    system ("pause");
#endif
    return 0;
 }


Caso de múltipla herança A - B e C

 #include <iostream>

 using namespace std;

 class base1 
         {
         public:
               base1() { cout << "Construindo base1\n"; }
               ~base1() { cout << "Destruindo base1\n"; }
        };

 class base2 
        {
        public:
               base2() { cout << "Construindo base2\n"; }
               ~base2() { cout << "Destruindo base2\n"; }
        };

 class derivada: public base2,public base1
        {
        public:
               derivada() { cout << "Construindo derivada\n"; }
               ~derivada() { cout << "Destruindo derivada\n"; }
        }; 

 int main()
 {
    derivada ob;// construindo e destruindo o objeto.
#ifdef WIN32
    system ("pause");
#endif
    return 0;
 }

Neste caso a sequência de inicialização segue ordem estabelecida na lista de herança. Mais explicitamente, temos a construção das bases: "base2" e "base1", nesta ordem respectivamente e depois a derivada. O que, automaticamente, nos revela a sequência de destruição na ordem inversa, ou seja: destroi-se a "derivada", depois "base1" e, finalmente, a estrutura da "base2".

Passando parâmetros para construtores da classe base

editar

Agora imaginemos que temos um conjunto de bases para uma classe que queiramos derivar, então podemos ter um construtor em cada base que precise de parâmetros para que possa ser invocado pela nossa classe. Como poderemos passar os parâmetros, uma vez que os mesmos só podem ser passados durante a inicialização da classe?

Para que possamos passar os parâmetros para as classes bases durante a inicialização do objeto da classe derivada temos o recurso de passagem de parâmetros pelo construtor. Basicamente, ele funciona como se passássemos valores para variáveis membro. Chamamos cada construtor na lista de passagem de valores, a sintax para declarar o corpo do construtor é a seguinte:

class Classe_derivada : public Base1, public Base2, ..., public BaseN
{ // Membros...
  public:
     Classe_derivada(lista_de_argumentos);
  // Outras funções...
};


 Classe_derivada::Classe_derivada(lista_de_argumentos) : Base1(lista_de_argumentos), Base2(lista_de_argumentos), ...BaseN(lista_de_argumentos);
 {
  //Corpo do construtor da classe derivada
 }

Este exemplo é um pouco mais complexo, atenção!

 #include <iostream>

 using namespace std;

 class base 
        {
        protected:
                  int i;
        public:
               base(int x) { i = x; cout << "Construindo base\n"; }
               ~base() { cout << "Destruindo base\n"; }
        };

 class derivada: public base 
        {
              int j;
        public:
               derivada(int x, int y): base(y) { j = x; cout << "Construindo derivada\n"; }// derivada usa x; y é passada em lista para a base.
               ~derivada() { cout << "Destruindo derivada\n"; }
               void mostrar() { cout << i << " " << j << "\n"; }
        };

 int main()
 {
    derivada ob(3, 4);
    ob.mostrar(); // mostra 4 3
#ifdef WIN32
    system ("pause");
#endif
    return 0;
 }

No exemplo, a o construtor da classe derivada é declarado com 2 argumentos (x e y). no entanto a função derivada() usa apenas um para inicializar a variável interna da classe, o segundo argumento é usado para passar o valor de inicialização para a classe base.

Vejamos mais um exemplo:

 #include <iostream>

 using namespace std;

 class base1 
    {
    protected:
              int i;
    public:
           base1(int x) { i = x; cout << "Construindo base1\n"; }
           ~base1() { cout << "Destruindo base1\n"; }
    };

 class base2 
    {
    protected:
              int k;
    public:
           base2(int x) { k = x; cout << "Construindo base2\n"; }
           ~base2() { cout << "Destruindo base2\n"; }
    };

 class derivada: public base1, public base2 
    {
         int j;
    public:
           derivada(int x, int y, int z): base1(y), base2(z)
                       { j = x; cout << "Construindo derivada\n"; }
           ~derivada() { cout << "Destruindo derivada\n"; }
           void mostrar() { cout << i << " " << j << " " << k << "\n"; }
    };

 int main()
 {
    derivada ob(3, 4, 5);
    ob.mostrar(); // mostra 4 3 5
#ifdef WIN32
    system ("pause");
#endif
    return 0;
 }

Superposição de funções

editar

Muitas vezes temos classes derivadas que executam uma determinada ação de forma distinta da mesma ação definida na classe base. Por exemplo, se temos uma classe "animal" e declaramos uma função chamada "mover" e depois declaramos duas derivadas: "ave" e "peixe" com a mesma função "mover" teremos uma incoerência devido ao fato de que peixes se movem de forma totalmente diferente de aves. Uma vez que peixes devem "nadar" e "aves" podem "voar" ou "andar" nosso modelo de objetos está incorreto.

Por questões de coerência semântica, porém, precisamos manter o mesmo nome para as funções das classes base e derivada em algumas construções. Isto é essencial devido a necessidade de criarmos objetos generalistas, por exemplo se tivermos classes "ave" e "peixe" abstraídas em uma base "animal", como vimos acima. Havendo estas condições, como poderemos criar comportamentos diferentes usando o mesmo nome para as funções?

A resposta está em uma das características que será muito útil quando quisermos usar de polimorfismo, que iremos abordar em capítulo específico mais adiante: a ocultação e superposição de funções da classe base a partir de uma classe derivada (conhecida como "overriding" em manuais de compiladores). Com este recurso podemos declarar em uma classe derivada uma função com nome e parâmetros idênticos a uma existente em uma classe base, porém com conteúdo diferente.

Vejamos o exemplo de código e teremos uma noção mais concreta do que foi explanado:

#include <iostream>

using namespace std;

 class animal
 {
   public:
      void comer();
      void mover();
      void dormir() { cout << "Dormindo..." << endl; }
 };
...
...
 class ave : public animal
 {
   public:
      void comer(){ cout << "Bicando..." << endl; }
      void mover(){ cout << "Voando..." << endl; }
 };
 ...
 ...
 class peixe : public animal
 {
   public:
      void comer(){ cout << "Mordendo..." << endl; }
      void mover(){ cout << "Nadando..." << endl; }
 };

int main()
{
  ave passarinho;
  peixe sardinha;

  passarinho.mover();
  sardinha.mover();

#ifdef WIN32
 system("pause");
#endif
 return 0;
}

Ao executar o programa gerado por este código percebemos que a mesma função: mover(), terá comportamento diferente quando invocada por objetos de classes diferentes. O programa mostrará a mensagem "Nadando..." para a função invocada pelo objeto sardinha e "Voando..." para a invocada pelo objeto passarinho. Aqui, o mecanismo é bem simples de se entender, quando cada objeto tem uma versão diferente para a mesma função é fácil para o compilador relacionar o objeto à classe que ele pertence e invocar a função apropriada.

Acessando funções superpostas da classe base

editar

O mecanismo para obter acesso às classes base a partir da classe derivada é intuitivo. Para isto usamos o operador de resolução de escopo, composto por um par de dois pontos "::", usando a seguinte sintaxe:

 <CLASSE>::<FUNÇÃO>(lista_de_parâmetros);

Ou seja, basta invocar a função informando qual é a versão específica que se deseja utilizar. Se tivermos uma classe "A" e outra "B" com uma função "Print()", por exemplo, e quisermos usar a função "Print()" da classe "B" fazemos:

 B::Print();

Talvez seja melhor visualizar um exemplo no código mais completo:

class B
{
  public:
         void Print()
              {
               cout << "Chamando Print() da classe B." << endl;
              }
};

class A : public B
{
  public:
         void Print()
              {
               cout << "Chamando Print() da classe A." << endl;
               B::Print();
              }
};

int main()
{ A ca;

  ca.Print();

  return 0;
}

Ver também

editar


  Esta página precisa ser reciclada (discuta).
Ao melhorá-la, você estará ajudando o Wikilivros.

Conceito

editar

Polimorfismo em linguagens orientadas a objeto, é a capacidade de objetos se comportarem de forma diferenciada em face de suas características ou do ambiente ao qual estejam submetidos, mesmo quando executando ação que detenha, semanticamente, a mesma designação.

O polimorfismo em C++ se apresenta sob diversas formas diferentes, desde as mais simples, como funções com mesmo nome e lista de parâmetros diferentes, até as mais complexas como funções virtuais, cujas formas de execução são dependentes da classe a qual o objeto pertence e são identificadas em tempo de execução.

Funções virtuais

editar
#include <iostream>

using std::cout;
using std::endl;

class Base {
public:
    // declaração da função virtual
    virtual void Quem_VIRTUAL()
    {
        cout << "Base\n";
    }
    // função comum
    void Quem_NAO_VIRTUAL()
    {
        cout << "Base\n";
    }
};

class Derivada : public Base {
public:
    // função virtual sobrescrita
    virtual void Quem_VIRTUAL()
    {
        cout << "Derivada\n";
    }
    // função comum sobrescrita
    void Quem_NAO_VIRTUAL()
    {
        cout << "Derivada\n";
    }
};

int main ()
{
    Base *ptr_base;
    Derivada derivada;

    ptr_base = &derivada;           // conversão implícita permissível
    ptr_base->Quem_VIRTUAL();       // chamada polimórfica (mostra: "Derivada")
    ptr_base->Quem_NAO_VIRTUAL();   // chamada comum, não-polimórfica (mostra: "Base")

    cout << endl;

    return 0;
}

Chamando múltiplas funções virtuais

editar

Funções virtuais e passagem por valor

editar

Construtor de cópia virtual

editar

Classe base virtual

editar

Consideremos o seguinte programa:

// Este programa contém um erro e não será compilado.
 #include <iostream>
 using namespace std;
 class base
    {
    public:
           int i;
    };

 class derived1 : public base // derived1 inherits base.
    {
    public:
           int j;
    };

 class derived2 : public base // derived2 inherits base.
    {
    public:
           int k;
    };

/* 
 * "derived3" herda características de "derived1" e "derived2". 
 * Isto significa que há duas cópias da base em "derived3"! 
 */
 class derived3 : public derived1, public derived2
    {
    public: 
            int sum;
    };

 int main()
 {
    derived3 ob;
    ob.i = 10; // Isto se torna ambíguo; A qual "i" estamos nos referindo???
    ob.j = 20;
    ob.k = 30;
    ob.sum = ob.i + ob.j + ob.k;// "i" ambíguo aqui, também
    cout << ob.i << " ";// também ambíguo, Qual "i"?
    cout << ob.j << " " << ob.k << " ";
    cout << ob.sum;
#ifdef WIN32
    system ("pause");
#endif
    return 0;
 }

As classes derived1 e derived2 são herdadas como classes base. A classe derived3 herda tanto de derived1 quanto de derived2. Como resultado temos 2 cópias da classe base presentes no objeto da derived3, por exemplo presente na linha ob.i=10; isto resulta numa ambiguidade e o programa não vai compilar.

Há duas maneiras para remediar a situação:

1. Aplicar o operador de resolução de escopo manualmente:

//Este programa usa resolução de escopo explicita para selecionar "i".
 #include <iostream>
 using namespace std;
 class base 
    {
    public:
           int i;
    };

 class derived1 : public base // derived1 inherits base.
    {
    public:
           int j;
    };

 class derived2 : public base // derived2 inherits base.
    {
    public:
           int k;
    };

 class derived3 : public derived1, public derived2 /* "derived3" herda as bases "derived1" e "derived2". Isto significa que há duas cópias de bases em "derived3"! */
    {
    public:
           int sum;
    };

 int main()
 {
    derived3 ob;
    ob.derived1::i = 10; 	// escopo resolvido, usa o "i" em "derived1".
    ob.j = 20;
    ob.k = 30;
    ob.sum = ob.derived1::i + ob.j + ob.k;		// escopo resolvido.
    cout << ob.derived1::i << " ";		// também resolvido aqui.
    cout << ob.j << " " << ob.k << " ";
    cout << ob.sum;
#ifdef WIN32
    system ("pause");
#endif
    return 0;
 }


2. A segunda maneira é através de classes bases virtuais:

Quando temos 2 ou mais objetos que são derivados da mesma base class, podemos prevenir múltiplas cópias da base class declarando a base class como virtual quando ela é herdada. Exemplificando:

 // Este program usa classes bases virtuais.
 #include <iostream>
 using namespace std;
 class base 
        {
        public:
               int i;
        };
 class derived1 : virtual public base // derived1 inherits base as virtual.
        {
        public:
               int j;
        };
 class derived2 : virtual public base // derived2 inherits base as virtual.
        {
        public:
               int k;
        };
 class derived3 : public derived1, public derived2 /* derived3 inherits both derived1 and derived2. This time, there is only one copy of base class. */
        { 
        public:
               int sum;
        };
 int main()
 {
    derived3 ob;
    ob.i = 10; // now unambiguous
    ob.j = 20;
    ob.k = 30;
    ob.sum = ob.i + ob.j + ob.k;// unambiguous
    cout << ob.i << " ";// unambiguous
    cout << ob.j << " " << ob.k << " ";
    cout << ob.sum;
    system ("pause");
    return 0;
 }

Repare que agora temos a palavra virtual antes da classe.


  Esta página precisa ser reciclada (discuta).
Ao melhorá-la, você estará ajudando o Wikilivros.

Friend functions

editar

O que é

editar

Friend é um atributo especial de acesso a classes. Com ele, declaramos que uma função é amiga de uma classe a qual não pertence.

Declarando que uma função (externa á classe) "friend" dentro de uma classe, permite que a função (a amiga) possa ler e manipular membros (variáveis e funções membro) "private" - privados e "protected" - protegidos (e claro "public" - publicas, mas isso já era permitido antes de serem "friends").

Considerando que o acesso a membros pode ser restrito para determinadas partes do código, podemos adotar uma postura mais flexível para funções que conhecemos como confiáveis e evitar os inconvenientes da restrição a membros por códigos que não provoquem problemas maiores a nossa estrutura da aplicação.

Declarar funções "friend"

editar

O processo para fazer com que funções fora do escopo da classe tenham acesso a membros sem nenhuma restrição é muito simples. Para isto apenas temos de colocar o protótipo da função externa dentro da classe precedido da palavra "friend". Desta forma o compilador passa a ignorar os atributos de restrição de acesso da classe quando a função acessa membros da mesma.

Vamos ao exemplo:

 // friend functions
 #include <iostream>
 using namespace std;
 class CRectangle 
 {
    int width, height;
  public:
    void set_values (int, int);
    int area () {return (width * height);}
    friend CRectangle duplicate (CRectangle);
 };
 void CRectangle::set_values (int a, int b) 
 {
  width = a;
  height = b;
 }
 CRectangle duplicate (CRectangle rectparam)
 {
  CRectangle rectres;
  rectres.width = rectparam.width*2;
  rectres.height = rectparam.height*2;
  return (rectres);
 }
 int main () 
 {
  CRectangle rect, rectb;
  rect.set_values (2,3);
  rectb = duplicate (rect);
  cout << rectb.area();
  system (pause);
  return 0;
 }

O uso de funções amigas deve ser evitado sempre que possível, pois diminui a identidade da orientação a objetos. Isto ocorre porque o uso desse mecanismo representa uma quebra no encapsulamento. Quando passamos a permitir que funções tenham acesso a membros restritos dos objetos fazemos com que agentes externos interfiram na autonomia dos mesmos. Isto pode dificultar a análise de programas muito extensos.

No caso da criação de procedimentos que tem a finalidade de modificar o conteúdo do objeto explicitamente, como nos casos de operadores e modificadores de comportamento, podemos usar as funções amigas "friends" para esta finalidade tomando os devidos cuidados para torná-las muito bem restritas as funções que devem executar. É muito importante observar se estas funções alteram dados dentro dos objetos que não podem ser modificados. Se as devidas precauções forem tomadas não haverá problemas no uso de funções "friend".

Friend classes

editar

Da mesma forma que podemos declarar funções como amigas de uma determinada classe, podemos declarar outra classe como sua amiga. Este artifício faz com que os membros da classe onde foi feita a declaração sejam acessíveis à declarada. Assim, a segunda classe passa a ter possibilidade de manipulação livre dos membros da outra.

Apesar da funcionalidade ser um pouco semelhante à que temos no uso das funções, quando declaramos uma classe como "friend" dentro de outra, teremos todas as funções da primeira com poderes de acesso aos membros da segunda. Esta característica requer cuidado redobrado quando operações da classe "friend" interferem no conteúdo da outra.

 // friend class
 #include <iostream>
 using namespace std;
 class CSquare;
 class CRectangle 
 {
    int width, height;
  public:
    int area ()
      {return (width * height);}
    void convert (CSquare a);      //consigo acessar porque é friend
 };
 class CSquare 
 {
  private:
    int side;
  public:
    void set_side (int a) {side=a;}
    friend class CRectangle;      //declaro friend class
 };
 void CRectangle::convert (CSquare a) 
 {
  width = a.side;
  height = a.side;
 }
 int main () 
 {
  CSquare sqr;
  CRectangle rect;
  sqr.set_side(4);
  rect.convert(sqr);
  cout << rect.area();
  system (pause);
  return 0;
 }


Nota:

  • Não há a propriedade de reciprocidade (ou reversão). O facto de uma função ou classe ser friend de uma classe não implica o contrário, ou seja, A ter B como friend não implica que B a tenha em mesma conta.
  • Outra propriedade é que não há transitividade. Se numa class A for declarado que a class B é friend. E depois que na classe B estiver declarado que a classe C é friend de B,…Isto não implica que A seja friend de C.

Conceituação

editar

As classes podem ser construídas uma dentro da outra para uma melhor organização do código. Esta possibilidade nos leva a implicações a serem consideradas durante a fase de implementação do código. Devemos verificar as particularidades de acesso a dados, o que nos leva a diversas particularidades devido ao escopo das mesmas.

No exemplo a seguir, temos a implementação básica de uma classe interna a outra:

class data     
{
 int n;
 char str[256];
  public:
  class idata
     { int x;
       int y;
       int z;
     };

     int getn();
     char *getstr();
     void setstr( const char *instr);
     void setn(int inn);
};

Uma vez que criamos o protótipo de objeto dentro de outra classe podemos usá-lo através do operador de resolução de escopo. Deveremos nos referir ao operador para acessar o corpo da primeira classe e depois o invocamos novamente para alcançar a outra. Desta forma, poderíamos usá-la como exemplificado abaixo:

data::idata ni;

É importante notar que objetos diferentes terão dados diferentes na região da classe interna, isso impede que usemos dados da classe mais externa dentro das classes internas. Devido a esta característica as funções dentro da classe interna não podem acessar dados da classe mais externa, a menos que sejam estáticos, o que não exige definição do objeto a qual eles pertencem. Portanto, o uso de classes internas permite um isolamento de parte dos dados e prover um tratamento diferenciado para os mesmos.

Modificando operadores

editar

A linguagem C++ possui os mesmos operadores presentes na linguagem C. Estes têm funções padronizadas e comportamentos semelhantes a seus parentes diretos em C. Esta característica a traz compatibilidade que é um requisito fundamental e adiciona uma nova funcionalidade chamada sobrecarga de operadores.

Quando operamos tipos nativos da linguagem, fazemos com funções específicas predefinidas e padronizadas. Como poderemos operar os nossos objetos que definimos com nossas classes? Simples: criamos as funcionalidades e as atribuimos a operadores já conhecidos, de forma a manter a idéia básica da operação embutida na simbologia.

Ao definir novas funções para os operadores padrão, na verdade não substituimos a sua função, apenas adicionamos mais uma função ao mesmo operador. Esta operação é chamada de sobrecarga de operador. O nome parece um pouco fora do comum, mas apenas reflete o comportamento da linguagem quando esta lida com a definição de vários tratamentos para o mesmo identificador, que, neste caso, é o símbolo do operador. Portanto, sobrecarga de operador é a definição de novas tarefas para o mesmo operador.

Definindo novas operações

editar

Digamos que temos uma classe chamada ponto, que define dois inteiros para um plano hipoteticamente definido. Este par de inteiros poderá representar uma coordenada neste plano formado por pontos espaçados um do outro. Sob estas condições, cada objeto desta classe será uma coordenada neste plano:

class ponto
{
    int x,y;
public:

    ponto(int a, int b)
    {
       x = a;
       y = b;
    } 
};

Se quisermos operar estes objetos não teremos como fazê-lo, pois não há meios de operar os objetos do tipo ponto. Nenhuma operação é possivel, pois a linguagem não define como operá-los. Cabe ao programador dizer ao compilador como ele deve efetuar a operação do novo tipo criado.

ponto p1(1,5), p2(3,4), Soma;

Soma = p1 + p2;

Ao tentar compilar este trecho de código o compilador retornará um erro por não conhecer a maneira de como operar este tipo de dado. Como criamos o tipo de dado, precisamos definir como fazer a soma do mesmo. Podemos fazer a seguinte definição:

class ponto
{
    int x,y;
public:

    ponto(int a, int b)
    {
       x = a;
       y = b;
    }
    ponto operator+(ponto p);
};

ponto ponto::operator+(ponto p)
{
    int a, b;
    a = x + p.x;
    b = y + p.y;
    
    return ponto(a, b);
}

A sintaxe desta definição, muitas vezes causa confusão, mas poderá ser facilmente entendida depois que tenhamos assimilado as idéias básicas por trás dela. Ela opera, aparentemente, apenas um dado de entrada, porém o operador deve somar dois. Como isto é possível? Observando mais atentamente o código poderemos entender:

Verificamos que, no código, nos referimos a x e y sem definir a qual objeto pertence. Acontece que a operação está ocorrendo dentro de um dos objetos, aquele imediatamente antes do operador. Esta é a primeira coisa a ter em mente: O operador "pertence" a um dos objetos que está sendo operado, sendo sempre aquele que o antecede. Com isso, só precisamos declarar o segundo dado a operar.

Podemos visualizar isto melhor, da seguinte forma:

P3 = P1 + P2;

Que pode ser entendido como a invocação da função:

P3 = P1.operator+( P2);

Agora podemos entender como acontece a invocação da função que define o operador. Observe que P1 contém o operador que recebe P2, fazendo o cálculo e devolvendo uma cópia do objeto resultante para P3. A sintaxe esconde o mecanismo para tornar o código mais simples de ser entendido quando tiver que ser lido.

Alocação dinâmica de memória

editar

Refere-se ao recurso, existente nas linguagens de programação, onde podemos deixar de reservar memória quando o programa ainda está na fase de desenvolvimento, quando não sabemos ainda de quanta memória o programa irá precisar para executar algum procedimento, e o fazemos durante a execução. Podemos alocar, ou seja, reservar uma quantidade de bytes para serem usados, de duas maneiras diferentes: Em tempo de compilação ou em tempo de execução, aqui tratamos do segundo caso.

O compilador reserva espaço na memória para todos os dados declarados explicitamente, mas se usarmos ponteiros precisamos colocar no ponteiro um endereço de memória existente. Para isto, podemos usar o endereço de uma variável definida previamente ou reservar o espaço necessário no momento que precisemos. Este espaço que precisamos reservar em tempo de execução é chamada de memória alocada dinamicamente.

Reservar dinamicamente é o caso em que não sabemos, no momento da programação, a quantidade de dados que deverão ser inseridos quando o programa já está sendo executado. Em vez de tentarmos prever um limite superior para abarcar todas as situações de uso da memória, temos a possibilidade de reservar memória de modo dinâmico. Os exemplos típicos disto são os processadores de texto, nos quais não sabemos a quantidade de caracteres que o utilizador vai escrever quando o programa estiver sendo executado. Nestes casos podemos, por exemplo, receber a quantidade de caracteres que o usuário digita e depois alocamos a quantidade de memória que precisamos para guardá-lo e depois o armazenamos para uso posterior.

O modo mais comum de alocação de memória é o de alocar certa quantidade de bytes e atribuí-la a um ponteiro, provendo um "array", ou vetor. Nos tópicos a seguir abordaremos estes e outros casos de uso de memória alocada dinamicamente.

Operador new

editar

Existem duas maneira de alocar memória dinamicamente em tempo de execução: Em "C" e em "C++" podemos usar as funções de alocação dinâmica de memória existentes nas bibliotecas padrões, ou usamos uma palavra chave da linguagem C++ chamada new. Tratemos, portanto, do segundo caso pois é específico da linguagem "C++".

De fato esta palavra chave é considerada um operador, sua função é reservar memória em tempo de execução do programa. Ela funciona de modo análogo às funções de alocação de memória da linguagem "C", nas quais, indicamos a quantidade de bytes que queremos alocar e as mesmas reservam o referido espaço necessário. Porém, o operador new tem uma sintaxe a ser seguida, a qual será abordada mais adiante.

Sem usar o operador "new" teremos um erro primário, criado inicialmente pela alocação de maneira incorreta. Vamos ver um exemplo:

Exemplo de alocação dinâmica de forma incorreta:

 #include <iostream>
 using namespace std;
 int main ()
 {
   int numTests;
   cout << "digite o numero de testes : ";
   cin >> numTests;
   int testScore[numTests]; //Erro de declaração
   return 0;
 }

De fato, no exemplo acima, podemos ver que numTests não tem valor definido durante a compilação, neste caso o compilador reporta um erro, normalmente exigindo um valor ou uma constante declarada como valor literal. A razão da exigência de ter uma constante (ou literal) é que vamos alocar memória para o "array" no ato da compilação, e o compilador necessita saber exatamente a quantidade de memória que deve reservar… porém, se o número entre colchetes é uma variável, o compilador não sabe quanta memória deveria reservar para alocar o "array".

Reformulando o exemplo anterior agora com dados dinâmicos:

 #include <iostream>
 using namespace std;
 int main ()
 {
   int numTests;
   cout << "Enter the number of test scores:";
   cin >> numTests;        
   int * iPtr = new int[numTests];           //colocamos um ponteiro no inicio da memória dinâmica

   for (int i = 0; i < numTests; i++) //Podemos preecher o espaço de memória da forma que quisermos
   {
      cout << "Enter test score #" << i + 1 << " : ";
      cin >> iPtr[i];
   }

   for (int i = 0; i < numTests; i++)  //Mostramos o que foi preenchido ...
      cout << "Test score #" << i + 1 << " is "<< iPtr[i] << endl;
   delete iPtr;
   return 0;
 }

Agora conseguimos criar um "array" onde é o utilizador poderá definir o tamanho do "array" e depois colocar o valor para cada um dos elementos.

O operador "new" retorna o endereço onde começa o bloco de memória que foi reservado. Como retorna um endereço podemos colocá-lo num ponteiro. Assim, teremos um meio de manipular o conteúdo da memória alocada toda vez que mencionarmos o ponteiro.

Verificamos no exemplo o uso do ponteiro que deve ser do mesmo tipo que o tipo de variável que é alocado dinamicamente:

int * iPtr = new int[numTests];
  • Temos o termo new. Que é um operador cuja função é alocar dinamicamente memória;
  • Temos o tipo da variável alocada dinamicamente: "int";
  • Repare que NÃO temos o nome do "array";
  • Uma vez que o "array" fica sem nome, para nos referirmos a cada elemento do mesmo teremos de usar o ponteiro.

Podemos inicializar o ponteiro de duas maneiras:

 int *IDpt = new int;  // Alocamos um único elemento dinâmico do tipo "int"
 *IDpt = 5;            // Atribuímos o valor 5 dentro da memória dinâmica

ou

 int *IDpt = new int(5);        //Alocamos o objeto int e inicializamo-lo com 5.
 char *letter = new char('J');  //Alocamos o objeto char e inicializamo-lo com 'J'.

Por outro lado, se quisermos criar um "array" de objetos "char" podemos proceder da forma como foi dado anteriormente:

 int *AIDpt = new char[4];    //Aloca 4 objetos de caracteres.
 AIDpt[0] = 'A'; // Podemos preencher os valores de cada elemento
 AIDpt[1] = 'M';
 AIDpt[2] = 'O';
 AIDpt[3] = 'R';

Operador Delete

editar

Se realmente não necessitamos mais dos dados que estão num endereço de memória dinâmica, devemos apagá-la! Necessitamos liberar essa memória através do operador delete – este operador entrega ao sistema operacional ou sistema de gerenciamento de memória os espaços de memória reservados dinamicamente.

A sintaxe é

 delete iPtr;

O operador delete não apaga o ponteiro mas sim a memória para onde o ponteiro aponta. Na verdade, como já dissemos anteriormente, a memória não é de fato apagada, ela retorna ao estado de disponível como memória livre para ser alocada novamente quando for necessário.

No caso de alocação de memória utilizando ponteiros locais a ação de criar o espaço através do operador new deve ser seguida de um delete depois que o espaço alocado não for mais necessário. Da mesma forma, se o espaço alocado continuará a ser necessário no resto do programa o endereço deverá ser mantido em ponteiro global ou deverá ser retornado para ser usado depois da execução da função.

O tempo de vida de uma variável criada dinamicamente é o tempo de execução do programa. Se fizermos um delete e tivermos um ponteiro que aponta para o endereço que não esteja mais alocado, não conseguiremos acessar nenhuma memória específica e, neste caso, acessaremos uma posição de memória qualquer, geralmente a que está armazenada dentro do ponteiro. Esse valor de memória muitas vezes é zero ou qualquer outro sem sentido, um valor que em algum momento foi criado pelo processador em qualquer operação anteriormente executada.

Outro problema comum: Se alocamos memória dinamicamente dentro de uma função usando um ponteiro local, quando a função termina, o ponteiro será destruído, mas a memória mantém-se. Assim já não teríamos maneira de acessar essa memória porque nós não temos mais o seu endereço! Além disso não teremos mais como excluí-la.

Caso não for excluída a memória dinâmica alocada com o operador new através do operador delete, o programa irá acumular memória alocada (reservada), o que levará à parada inesperada do programa por falta de memória para alocação quando outra operação new for solicitada e não haver mais espaço para alocar memória.

Temos este exemplo:

 void myfunction()
 {
    int *pt;
    int av;
    pt = new int[1024];
    ....
    ....
    //Nenhum "delete" foi chamado...
 } 
 int main()
 {
    while (0) 
    {
        myfunction();
    }
    return 0;
 }

Quando a função “myfunction” é chamada a variável “av” é criada na pilha e quando a função acaba a variável é perdida. O mesmo acontece com o ponteiro pt, ele é uma variável local. ou seja quando a função acaba o ponteiro também termina e é perdido. Porém o espaço de memória alocado dinamicamente ainda existe. E agora não conseguimos apagar esse espaço porque não temos mais o ponteiro e a única maneira que tínhamos para saber onde ele estava era através do ponteiro que foi perdido quando a função foi finalizada.

Analisando o programa como um todo temos, à medida que o programa continua a executar, mais e mais memória que será perdida no espaço de alocação dinâmica controlado pelo sistema operacional. Se o programa continuar deixaremos de ter memória disponível e o programa deixará de operar. Nos sistemas operacionais atualmente em uso o programa será interrompido e uma mensagem de erro será retornada para o usuário.

Retornando um ponteiro para uma variável local

editar

Analisemos o código abaixo, no qual há um erro:

 #include <iostream>
 using namespace std;
 char * setName();
 int main (void)
 {
   char* str = setName(); 	//ponteiros para a função
   cout << str; 	        //imprimo o valor do ponteiro?
   return 0;
 }
 char* setName (void)
 {
   char name[80];             // vetor criado na pilha, o mesmo deixará de existir quando 
                              // a função acabar.
   cout << "Enter your name: ";
   cin.getline (name, 80);
   return name;              // E aqui temos o erro... o vetor name não poderá ser lido 
                             // quando a função retornar , logo pode gerar um erro em 
                             // tempo de execução.
 }


Neste código podemos ver como os ponteiros mal administrados podem causar falhas que podem levar o programa a abortar em tempo de execução. Como está escrito nos comentários do código, o conteúdo do vetor name deixará de ser utilizável, nem para operações de leitura ou, principalmente em operações de escrita. Neste segundo caso provocará um acesso a memória não autorizado, o programa poderá travar ou abortar.

A solução é estender o tempo de vida do ponteiro e do seu endereço destino. Uma solução possível seria tornar esse "array" global, mas existem alternativas melhores.

Retornando um Ponteiro a uma Variável Local Estática

editar

No código a seguir temos uma alternativa para o uso de vetores e retorno de seu conteúdo:

 #include <iostream>
 using namespace std;
 char * setName();
 int main (void)
 {
   char* str = setName();
   cout << str;
   return 0;
 }
 char* setName (void)
 {
   static char name[80]; 	//crio como static
   cout << "Enter your name: ";
   cin.getline (name, 80);
   return name;
 }

A diferença é que usamos a palavra static. Este modificador static, quando utilizado dentro de uma função para declarar variáveis, promove o vetor à categoria de permanente durante a execução do programa. Sendo assim, teremos como utilizar o endereço do ponteiro str tanto dentro como fora da função setName() e assim evitaremos de acessar uma memória não existente.

Retornando um Ponteiro com um valor de memória Criada Dinamicamente

editar

Outra alternativa, talvez melhor:

 #include <iostream>
 using namespace std;
 char * setName();
 int main (void)
 {
  char* str= setName();
  cout << str;
  delete str;            //faço o delete depois que o conteúdo do ponteiro não é mais necessário.
  return 0;
 }
 char* setName (void)
 {
  char* name = new char[80];     //crio ponteiro chamado de name e dou o valor do endereço da memoria dinâmica
  cout << "Enter your name: ";
  cin.getline (name, 80);
  return name;
 }

Isto funciona porque o ponteiro retornado da função setname() aponta para o "array" cujo tempo de vida persiste até que usemos o delete ou que o programa termine. O valor do ponteiro local name é atribuído na função main() a outro ponteiro str, desta forma podemos manipular o conteúdo da memória alocada até que não precisemos mais dele.

Este é um exemplo onde diferentes ponteiros apontam para o mesmo endereço. Na verdade podemos atribuir a qualquer ponteiro o endereço alocado, isso nos dá a possibilidade de manipular os dados armazenados na memória dinamicamente alocada em qualquer local onde seu endereço seja conhecido.

Para manter a segurança do código é sempre aconselhável que tenhamos um controle rígido sobre os ponteiros e seus valores (endereços armazenados). É imprescindível que os espaços de memória apontados por ponteiros sejam eliminados (liberados), quando não forem mais necessários, para evitar que o programa continue a reservar memória e não liberar, levando ao esgotamento da memória ou crescimento desnecessário do uso de memória por parte do programa.

Alocar dinamicamente Arrays (Vetores)

editar

Ora o que fizemos antes com variáveis, podemos fazer com "arrays", como já introduzimos brevemente anteriormente. Vejamos um exemplo:

int *pt = new int[1024];    //Aloca um Array (Vetor) de 1024 valores em int
 double *myBills = new double[10000];    
 /* Isso não significa que temos o valor 10000, mas sim que alocamos 10000 valores em double para guardar o monte de milhares de contas que recebemos mensalmente. */

Notar a diferença:

int *pt = new int[1024];    //Aloca um vetor que pode ter 1024 valores em int diferentes
int *pt = new int(1024);    //Aloca um único int com valor de 1024 (uma variável inicializada)

Para utilizar o delete em arrays usamos o ponteiro com o valor da primeira célula do array:

 delete pt;
 delete myBills;

A melhor maneira para alocar um "array" dinamicamente e inicializá-lo com valores é usar o loop:

int *buff = new int[1024];
 for (i = 0; i < 1024; i++) 
 {
    *buff = 52; //Assimila o valor 52 para cada elemento
     buff++;
 }
 ...
 ...
 buff -= 1024;
 delete buff;

	//ou se quisermos desta maneira:
 int *buff = new int[1024];
 for (i = 0; i < 1024; i++) 
 {
    buff[i] = 52; //Assimila o valor 52 para cada elemento
 }
 ...
 ...
 delete buff;

Note que a segunda forma, além de mais elegante, não altera o valor do vetor e assim facilita a remoção da alocação usando o delete.

Dangling Pointers

editar

Quando temos um ponteiro que não tem em seu conteúdo um valor de memória válido, o qual pertence ao programa sendo executado, temos um problema grave, principalmente se isso ocorreu por engano. Isso pode ser um problema difícil de identificar, principalmente se o programa já está com um certo número de linhas considerável. Vejamos um exemplo:

int *myPointer;
 myPointer = new int(10);
 cout << "O valor de myPointer e " << *myPointer << endl;
 delete myPointer;
 *myPointer = 5; //Acesso indevido a uma área não identificada.(Lembre-se que o ponteiro perdeu o valor válido de memória que apontava antes do '''delete'''. Agora a área de memória não pertence mais ao programa.).
 cout << "O valor de myPointer e " << *myPointer << endl;

Neste exemplo liberamos a memória dinâmica, mas o ponteiro continua a existir. Isto é um "bug", e muito difícil de detectar. Acontece que se essa memória for acessada ou escrita será corrompida. A melhor maneira de evitar isso é, depois do delete, fazer o ponteiro apontar para zero, fazê-lo um ponteiro nulo. Depois disso se tentarem usar o ponteiro iremos ter a "run time exception" e o "bug" poderá ser identificado

Assim, corrigindo o código anterior ficaríamos com:

int *myPointer;
 myPointer = new int(10);
 cout << "The value of myPointer is " << *myPointer << endl;
 delete myPointer;
 myPointer = 0;
 *myPointer = 5;    //Essa instrução vai causar uma "run-time exception", agora.
 cout << "The value of myPointer is " << *myPointer << endl;

Verificar a existência de memória para dinâmica

editar

Na alocação dinâmica temos de nos certificar de que a alocação no heap foi feita com sucesso e podemos ver isso de duas maneiras:

  • Uma são as exceções (este é o método defaut)
 bobby = new int [5];  // se isso falhar, é lançada uma "bad_alloc" (exception)

vamos ver este caso quase no ultimo capitulo- isto é uma capítulo avançado

  • nothrow, aqui no caso de não se conseguir a memória retorna um ponteiro nulo, e o programa continua.
 bobby = new (nothrow) int [5];

supostamente este método pode ser tedioso para grandes projetos


Vamos ver um exemplo com o caso de nothrow

 // rememb-o-matic
 #include <iostream>
 using namespace std;
 int main ()
 {
  int i,n,*p;
  cout << "Quantos números você deseja digitar? ";
  cin >> i;
  p= new (nothrow) int[i];            //criamos I variaveis na execução
  if (p == 0)
    cout << "Erro: a memória não pode ser alocada."<<endl;
  else
  {
    for (n=0; n<i; n++)
    {
      cout << "Digite o número: ";
      cin >> p[n];
    }
    cout << "Você digitou os seguintes números: ";
    for (n=0; n<i; n++)
      { cout << p[n];
        if (n<i-1) 
            cout << ", ";
      }
    cout<<endl;
    delete p;
  }
  return 0;
 }

Neste simples exemplo implementamos uma forma de alocar posições de memória para um vetor de 'n' elementos, de forma que o usuário possa digitar a quantidade de elementos que ele vai digitar e depois proceda com a entrada dos valores. Veja que o número de elementos é determinado pelo próprio usuário no início do programa, logo depois de indicar a quantidade de números a ser digitada o programa pede que seja alocada a quantidade de memória necessária para guardar os números a serem digitados. Neste momento, caso algum erro de alocação ocorrer, o sistema retorna o ponteiro nulo e podemos então, abortar o prosseguimento do programa, mostrando uma mensagem de erro em seguida para que o usuário fique ciente que ocorreu um erro.

  Esta página precisa ser reciclada (discuta).
Ao melhorá-la, você estará ajudando o Wikilivros.


Uma exception é um erro que ocorre em tempo de execução. Podemos lidar com estes erros e criar rotinas para muitos deles, o que nos permite automatizar muitos erros que antes teriam de ser emendados à mão.

Standard Exceptions

editar

A biblioteca de C++ contém exceptions já definidas para erros comuns. Elas são divididas em dois setores: Erros lógicos e Erros de tempo de execução.

Aqui vemos um exemplo de código usando exceptions de erros lógicos:

 #include <iostream>
 #include <stdexcept> // std::invalid_argument
 #include <bitset>
 #include <string>

 int main (void)
 {
   try
   {
     // O construtor de bitset joga uma invalid_argument se inicializado com uma
     // string contendo caracteres diferentes de 0 e 1
     std::bitset<5> mybitset (std::string("01234"));
   }
   catch ( const std::invalid_argument& ia )
   {
     std::cerr << "Invalid argument: " << ia.what() << '\n';
   }
   return 0;
 }
  Esta página precisa ser reciclada (discuta).
Ao melhorá-la, você estará ajudando o Wikilivros.


O propósito dos namespace é localizar os identifiers (os nomes) por forma a evitar que haja apenas um, para evitar colisões. Por exemplo eu poderia criar uma função com um determinado nome e depois vir a saber que esse mesmo nome existia na biblioteca. E isto pode ocorrer bem freqüentemente quando temos vários programadores contribuindo para o mesmo projeto e ainda mais quando se recorre a bibliotecas para usar código criado por outros.

O que o namespace permite é continuar a termos o mesmo nome mas irá fazer a diferenciação pela detecção do contexto de aplicação para cada nome.

  Esta página precisa ser reciclada (discuta).
Ao melhorá-la, você estará ajudando o Wikilivros.

Os templates permitem a criação de código reusado, usando templates é possível criar funções e classes genéricas. Assim o tipo de dados usados pelas funções são parâmetros. Podemos criar um template para soma, e depois enviamos que tipo de dados queremos somar, podemos até utilizar a sobrecarga de operadores para tal fim.

Funções genéricas

editar
 template <class Ttype> ret-type func-name(parameter list)
 {
 // body of function
 }

vamos dar o exemplo

 // Function template example.
 #include <iostream>
 using namespace std;
 template <class X> void swapargs(X &a, X &b)// This is a function template.
    {
        X temp;
        temp = a;
        a = b;
        b = temp;
    }
 int main()
 {
    int i=10, j=20;
    double x=10.1, y=23.3;
    char a='x', b='z';
    cout << "Original i, j: " << i << ' ' << j << '\n';
    cout << "Original x, y: " << x << ' ' << y << '\n';
    cout << "Original a, b: " << a << ' ' << b << '\n';
    swapargs(i, j); // swap integers
    swapargs(x, y); // swap floats
    swapargs(a, b); // swap chars
    cout << "Swapped i, j: " << i << ' ' << j << '\n';
    cout << "Swapped x, y: " << x << ' ' << y << '\n';
    cout << "Swapped a, b: " << a << ' ' << b << '\n';
    system ("pause");
    return 0;
 }

Aqui criamos uma template para uma função que troca os valores, e essa função pode ser usada quer tendo ints, doubles ou chars. nós utilizamos o X como data type. neste caso temos swapargs() como uma função genérica. O compilador automaticamente cria 3 versões para a função swapargs(). portanto os templates não são bem funções que funcionam para as vários tipos, mas antes os templates permitem poupar escrita ao programador para não ter de especificar cada tipo.

template function ou generic function (é uma definição de uma função precedida pela statement template

especialização == é quando o compilador cria a versão especifica da função. também é chamada de generated function. o ato de gerar a função é referido como instantiating.

há quem prefira ter a declaração do template da seguinte forma

 template <class X> 
 void swapargs(X &a, X &b)// This is a function template.
    {
        X temp;
        temp = a;
        a = b;
        b = temp;
    }

O que se torna bem parecido com o modelo utilizado anteriormente.

Uma função com dois tipos genéricos:

 #include <iostream>
 using namespace std;
 template <class type1, class type2>
    void myfunc(type1 x, type2 y)
            {
            cout << x << ' ' << y << '\n';
            }
 int main()
 {
    myfunc(10, "hi");
    myfunc(0.23, 10L);
    system ("pause");
    return 0;
 }

Repare que temos dois tipos de dados diferentes na mesma função.

Sobrecarregando explicitamente uma função genérica

editar

apesar de uma função genérica poder ser overload automaticamente se necessário, nós podemos explicitar. a isso chamamos deexplicit specialization

  • list
  • set
  • unordered_set
  • map
  • unordered_map
  Esta página precisa ser reciclada (discuta).
Ao melhorá-la, você estará ajudando o Wikilivros.

código fonte compilar desbloquear Cc++

Compilação é o processo de "tradução" do programa escrito em uma linguagem de programação para um formato no qual o computador entenda. A compilação gera um ficheiro - arquivo em português brasileiro - binário (executável) a partir do código fonte.

A tradução do código para o computador

editar

Em C e C++ são 3 os programas usados para fazer a tradução do código fonte (que são as linhas de código que o programador escreve) num ficheiro executável, que o computador pode executar:

  1. Preprocessor - pré-processador
  2. Compiler - compilador
  3. Linker - lincador (ou ligador)

Pré-processador

editar

Este é um programa que busca no código fonte – no código que escrevemos – por diretivas que foram dirigidas a ele, ou seja linhas iniciadas com “#”, assim o pré-processador sabe que aquela instrução é dirigida para ele. No exemplo tínhamos:

#include <iostream>

Então o pré-processador inclui ficheiros localizados no ficheiro iostream. Então tínhamos o código fonte que depois é transformado num outro código fonte de acordo com as diretivas pré-processadas.

Essencialmente, o pré-processador é um processador de macros para uma linguagem de alto nível.[1]

Compilador

editar

Compilador é um programa que pega código fonte preprocessado e o traduz em instruções de linguagem de máquina, linguagem que o computador entende. Estas são guardadas num arquivo a parte, chamado de "object file" e tem a extensão .o ou .obj dependendo do compilador. Existem diferentes compiladores para diferentes linguagens de programação. Essa tradução é feita se o código estiver na linguagem que o compilador compilar. Existem regras de escrita e de gramática. No caso de existir um erro de sintaxe, então dará um erro de compilação.

Linker

editar

Apesar do nosso "object file" ter as instruções em linguagem máquina, o computador ainda não poder correr como um programa. A razão é que é necessário outro código da biblioteca, que é o código do run-time library, que é para as operações comuns tipo a tradução o input do teclado ou a capacidade para interagir com hardware externo tipo o monitor para apresentar uma mensagem. Estas bibliotecas run-time costumam já estar instaladas com o sistema operacional, caso isso não aconteça teremos de fazer o download delas.

Então o resultado da combinação do "object file" com as partes necessárias da biblioteca run-time fazem finalmente a criação de um ficheiro executável com a extensão .exe

Processo de compilação

editar

Em primeiro lugar, vamos escrever um código que está na linguagem C++ e vamos gravar esse código todo num ficheiro, que é uma quantidade de memória no computador. Esse ficheiro fica com a terminação “.CPP” ou .CP ou C.. Chama-se a este conjunto de linhas que escrevemos de, código fonte ou source code (pode ter a terminação “.c”, “.cpp”, e “.cc” - estas extensões dependem do compilador que se utiliza). Esse "source code" é de alguma forma críptica e para alguém que não saiba de c++ no entanto é aquilo que neste momento chamamos de humam readable form.

Para tornar o nosso source code num programa usamos um compilador que irá produzir um "object file". Este ficheiro tem normalmente a extensão. OBJ porém ainda não temos um programa executável. Para isso vamos utilizar o linker. Uma das vantagens do c++ é que usufrui de uma estrutura multi-file. A linguagem permite compilação separada, onde partes do programa total podem estar numa ou mais source files e estes podem ser compilados independentemente de cada um. A ideia é que o processo de compilação produz files que depois podem ser linked together usando um editor de link ou loads que o sistema provem.

Os programas feitos em c++ são tipicamente ficheiros .OBJ ligados uns aos outros com uma ou mais libraries. Estas são uma coleção de linkable files que nós criámos ou que foram fornecidas (vêm com o compilador ou compramos).

Depois de se fazer este linking é que obtemos o arquivo executável. No Windows, ele tem terminação “.exe”. Assim, o sistema operacional reconhece o programa como independente.

Os compiladores atuais incluem também pré-compiladores, (ou pré-processadores) (antes eram software independente, extra), estes pré-compiladores vão fazer alterações ao código fonte, que basicamente consistem em eliminar pedaços de código que escrevemos, e/ou substituir pedaços de código que escrevemos por outro (copy-paste), enfim, é alterar o código fonte por outro código fonte. Depois é que se compila.

Estas são as palavras reservadas do c++:

alignas (since C++11) alignof (since C++11) and
and_eq asm auto
bitand bitor bool
break case catch
char char16_t (since C++11) char32_t (since C++11)
class compl const
constexpr (since C++11) const_cast continue
decltype (since C++11) default delete
do double dynamic_cast
else enum explicit
export extern false
float for friend
goto if inline
int long mutable
namespace new noexcept (since C++11)
not not_eq nullptr (since C++11)
operator or or_eq
private protected public
register reinterpret_cast return
short signed sizeof
static static_assert (since C++11) static_cast
struct switch template
this thread_local (since C++11) throw
true try typedef
typeid typename union
unsigned using virtual
void volatile wchar_t
while xor xor_eq

algarimo 100 sorte jogo Cc++ Estes são caracteres que são difíceis de serem expressos de outra forma em um código fonte. Todos eles são precedidos de uma barra invertida. A Tabela 1 apresenta a lista de sequências de escape.

Controle/Caracter Sequência de escape Valor ASCII
Nulo (null) \0 00
Campainha (bell) \a 07
Retrocesso (backspace) \b 08
Tabulação horizontal \t 09
Nova linha (new line) \n 10
Tabulação vertical \v 11
Alimentação de folha (form feed) \f 12
Retorno de carro (carriage return) \r 13
Aspas (") \" 34
Apóstrofo (') \' 39
Interrogação (?) \? 63
Barra invertida (\) \\ 92

Tabela 1 - Lista de Sequências de Escape em C++

Programa que gera a tabela ASCII do C++ (programa escrito em c++). Nenhum dos 2 programas mostra a tabela inteira. O programa mostra alguns caracteres que o programa 2 não mostra. De modo geral, faltam alguns caracteres.


 

Esta página é um esboço de informática. Ampliando-a você ajudará a melhorar o Wikilivros.

Programa 1

editar
#include <stdio.h>
#include <iostream>
using namespace std;


int main()
{
   //CARACTER " " (ENTER) = DECIMAL 10 HEXADECIMAL A;
   

    int s = -127;
    
    cout<<" CARAC     DEC       HEX\n\n";
    for ( char i = -127; i<127; i++ ) 
    {
        cout<<"  "<<i<<"       "<<s<<"     ";
        printf("%X",s);                             
        cout<<"\n";
        s++;
    }
    cout<<"\n    CARAC     DEC       HEX\n\n";
    cout<<"  \x122 ENTER \x122    10        A\n\n";
    system ("PAUSE");
    return 0;
}

Programa 2

editar
#include <stdio.h>
#include <iostream>
int main()
{
   int x;

   printf("   DEC\tHEX\tA  DEC\tHEX\t");
   printf("   DEC\tHEX\n");
   for(x=-173;x<360;x++)
   {
      printf("%c  %3i\t%2X\t",x,x,x);
      printf("%c  %3i\t%2X\t",x+32,x+32,x+32);
      printf("%c  %3i\t%2X\t",x+64,x+64,x+64);
      printf("%c  %3i\t%2X\n",x+96,x+96,x+96);
   }
   system ("PAUSE");
   return 0;
}

Origem: Wikipédia, a enciclopédia livre.

C++11, anteriormente conhecido por C++0x é o novo padrão para a linguagem de programação C++. Ele substitui o antigo padrão do C++, o ISO/IEC 14882, que foi publicado em 1998 e atualizado em 2003. Estes predecessores foram informalmente chamados C++98 e C++03. O novo padrão incluirá muitas adições ao núcleo da linguagem (sua implementação principal), e estenderá a biblioteca padrão do C++, incluindo a maior parte da biblioteca do chamado C++ Technical Report 1 — um documento que propõe mudanças ao C++ — com exceção das funções matemáticas específicas.

Esse nome é uma referência ao ano no qual o padrão será lançado. O comitê pretendia introduzir o novo padrão em 2009,1 a partir do que o então chamado "C++0x" passaria a se chamar "C++09", o que significa que o documento deveria estar pronto para a ratificação dos membros do comitê até o final de 2008. Para cumprir o prazo, o comitê decidiu focar seus esforços nas soluções introduzidas até 2006 e ignorar novas propostas.2 porém ele ficará pronto apenas em 2010.

Linguagens de programação como o C++ utilizam um processo evolucionário para desenvolverem suas definições. Tal processo inevitavelmente culmina em problemas de compatibilidade com código pré-existente, o que ocasionalmente aconteceu durante o processo de desenvolvimento do C++. Entretanto, de acordo com o anúncio feito por Bjarne Stroustrup — inventor da linguagem C++ e membro do comitê — o novo padrão será quase completamente compatível com o padrão atual.3

Novas classes:

  1. http://www.dca.fee.unicamp.br/cursos/EA876/apostila/HTML/node150.html O pré-processador C