Prévia do material em texto
PDF gerado usando o pacote de ferramentas em código aberto mwlib. Veja http://code.pediapress.com/ para mais informações.
PDF generated at: Fri, 06 Dec 2013 10:37:21 UTC
Programar em C++
Conteúdo
Páginas
Objetivo 1
Por que C++? 1
Diferenças entre C e C++ 2
Introdução 3
Alô, Mundo! 6
Variáveis e constantes 12
Ponteiros 18
Vetores 20
Estruturas 29
Operadores 34
Decisão e controle de fluxo 37
Laços 39
Funções 42
Referências de dados 44
Entrada e saída de dados 49
Entrada e saída de dados 2 58
Manipulando strings 73
Classes 81
Encapsulamento 96
Herança 99
Polimorfismo 112
Friend 117
Classes internas 119
Sobrecarga de operadores 120
Alocação dinâmica de memória 122
Exceções 129
Namespace 129
Templates 129
Compilação 131
Lista de Palavras Reservadas do C++ 133
Lista de Sequências de Escape 133
Tabela ASCII 134
C++11 135
Referências
Fontes e Editores da Página 136
Fontes, Licenças e Editores da Imagem 137
Licenças das páginas
Licença 138
Objetivo 1
Objetivo
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.
Por que C++?
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:
• Compiladores;
• Editores;
• Ferramentas de programação;
• Jogos;
• Programas de redes.
Até ao momento foram realizadas 3 grandes revisões à linguagem:
Por que C++? 2
•• 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).
Diferenças entre C e C++
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 é somente um esboço.
Ampliando-a você ajudará a melhorar o Wikilivros.
Introdução 3
Introdução
Pensando no código
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
Observando o modo de programar que as linguagens oferecem desde os primórdiosda 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
Introdução 4
tratado por uma abordagem orientada a objetos ou a procedimentos.
Um pouco sobre 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 traz 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 tem 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 objectos 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:
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)
• Abstracção de tipos de dados - exemplo: Ada (anos 80)
• Programação Orientada a Objectos - exemplos: C++, Java, Delphi (Object Pascal) entre outras. (décadas
80-90-2000)
Introdução 5
Objetos
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".
Alô, Mundo! 6
Alô, Mundo!
Olá mundo!
É 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 caracteresserá 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");
Alô, Mundo! 7
#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
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;
system ("pause");
return 0;
}
Comentários no programa
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
Alô, Mundo! 8
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
#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.
Alô, Mundo! 9
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 directivas de preprocessamento não terminam com o ponto e vírgula como nas instruções.
Namespace
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 objectos 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"
int main(){}
Como na linguagemC, 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;
}
Alô, Mundo! 10
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"; system ("pause"); 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)
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;
Alô, Mundo! 11
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")
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
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?
Variáveis e constantes 12
Variáveis e constantes
Constantes
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.1. Resguarda da inviolabilidade de valores apontados por ponteiros;
2.2. Auxílio na compreensão das características de funções, durante a implementação.
Simbólicas
Constantes simbólicas podem ser criadas com as diretivas do preprocessador #define. Neste modoos 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
...
...
Variáveis e constantes 13
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
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
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
Variáveis e constantes 14
evita que o mesmo programa seja compilado em ambientes diferentes e tenha comportamento diferente.
Variáveis
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.
Tipos
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
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
Variáveis e constantes 15
const
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
Uma variável "volátil", como a própria expressão sugere, é uma variável que pode ser modificadasem 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;
Variáveis e constantes 16
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.
Variáveis e constantes 17
Nomeando tipos
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 aboradaremos 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;
Variáveis e constantes 18
...
...
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
Ponteiros
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 idéia 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 em
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 *
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;
Ponteiros 19
O operador &
Na linguagem "C", o operador & tem duas funções básicas, funciona como operador da função lógica 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"
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óriaonde 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:
Ponteiros 20
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
Vetores e Matrizes
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 21
Vetores
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
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
Vetores 22
equivalente a soma de suas variáveis internas para ser alocado na memória.
Declarando arranjo
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];
system (“pause”);
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
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
É exatamente como declarar uma variável com duas diferenças:
1.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.2. Teremos de atribuir logo o valor na declaração, ou seja, é fazer a iniciação
Exemplo:
const int numTests = 3;
Vetores 23
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]
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
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
Vetores 24
Caracter array
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
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
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.
Vetores 25
Const Constant arrays
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
#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;
system (“pause”);
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;
}
system (“pause”);
return 0;
}
Ou melhor ainda podemos usar uma constante, para tornar o nosso código mais abstracto.
#include <iostream>
using namespace std;
Vetores 26
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;
}
system (“pause”);
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
system ("pause");
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
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++)
Vetores 27
{
cout << "Test score #" << i + 1 << ": " << testScore[i] << endl;
}
system ("pause");
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);
system ("pause");
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
// 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";
Vetores 28
}
intmain ()
{
int a[] = {5, 10, 15};
printarray (a,3); //passo array como argumento
system (“pause”);
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”;
}
system (“pause”);
return 0;
}
void doubleThem(int a[], int size)
{
int i;
for (i = 0; i < size; i++)
{
a[i] = 2 * a[i];
}
}
Estruturas 29
Estruturas
Breve revisão
Conceito
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
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];
};
Estruturas 30
int main()
{
Estrutura MinhaVariavelComposta;
...
...
...
return 0;
}
Acessando dados internos
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++
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.
Estruturas 31
Construtores
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;
const int MAX = 3;
struct Person
{
string name;
int height;
};
int main ()
{
Person p1;
cout << "The person's name is " << p1.name << " and height is " << p1.height << endl;
system (“pause”);
return 0;
}
O resultado é:
The person's name is and height is -858993460
Aqui é criado um defaut constructor no momento em que criamos a instância p1 ie 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!
Constructor sem argumentos
Podemos ter um construtor sem argumentos que ao contrário do defaut constructor designa valores defaut às
variáveis membro.
struct Person
{
string name;
int height;
Person() //construtor sem argumentos
{
name = "No name assigned";
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
Estruturas 32
#include <iostream>
#include <string>
using namespace std;
const int MAX = 3;
struct Person {
string name;
int height;
Person()
{
name = "No name assigned";
height = -1;
}
};
int main ()
{
Person p1;
cout << "The person's name is "<< p1.name << " and height is " << p1.height << endl;
system (“pause”);
return 0;
}
Repare que demos valores defaut á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ão.
Constructor com argumentos
Termos um constructor sem argumentos é um melhoramento face ao defaut constructor pois agora temos valores
defaut 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 e a correr. E realmente podemos fazer se passarmos
argumentos.
#include <iostream>
#include <string>
using namespace std;
const int MAX = 3;
struct Person
{
string name;
int height;
Person() //constructor sem argumentos
{
name = "No name assigned";
height = -1;
}
Person(string s, int h) //constructor com 2 argumentos
{
name = s;
height = h;
}
Estruturas 33
};
int main ()
{
int metro;
string strName;
cout << "Entre com o nome da pessoa: ";
getline(cin, strName);
cout << "Enter height in metro: ";
cin >> metro;
cin.ignore();
Person p1(strName,metro);
cout << "The person's name is " << p1.name << " and height is " << p1.height << endl;
system (“pause”);
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;
const int MAX = 3;
struct Person {
string name;
int height;
Person(); //construtor sem argumento
Person(string, int); //construtor com dois parâmetros, apenas é
necessário dizer o tipo dos parâmetros – o nome não é necessário)
};
Person::Person()
{
name = "No name assigned";
height = -1;
}
Person::Person(string s, int h)
{
name = s;
height = h;
}
int main ()
{
Estruturas 34
int metro;
string strName;
cout << "Enter person's name: ";
getline(cin, strName);
cout << "Enter height in inches: ";
cin >> metro;
cin.ignore();
Person p1(strName, inches);
cout << "The person's name is " << p1.name << " and height is " << p1.height << endl;
system (“pause”);
return 0;
}
•• Vamos ver a função main(): declarámos 2 variáveis uma int e outra string. Pedimos para a pessoa escrever o nome
e colocámos o valor na variável string, depois pedimos a altura e colocámos na variável int. Depois chamámos o
construtor com dois argumentos e passamos as variáveis anteriores como argumentos. Por fim mandámos
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
Operadores
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
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
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
Operadores 35
operações e alterá-las quando nos seja conveniente.
Entendendo o operador
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
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 );
Operadores 36
Operadores aritméticos
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:
<source lang=cpp>
Resultados: 10, 0, 2.55, 2, 1.
O quarto resultado é '2' pois 50 é o código decimal deste caracter.
Tipo de retorno
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
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
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.
Operadores 37
Divisão real
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 quea 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.
Decisão e controle de fluxo
Decisão em C++
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
if
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
Decisão e controle de fluxo 38
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 }";
}
else
É 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 no ecrã 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.
Laços 39
Laços
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.
While
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
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
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
do {
comando;
} while (condição);
Laços 40
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;
}
For
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
for (parâmetro1;parâmetro2;parâmetro3)
comando;
Onde:
• parâmetro1: campo destinado para qualquer comando que deve ser executado, uma única vez, logo no início do
laço.
• parâmetro2: 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.
• parâmetro3: 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 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
Laços 41
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;
}
DicasBloco de Comandos
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.
}
Funções 42
Funções
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
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;
Funções 43
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.
Funções 44
Parâmetros default (padrão)
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.
Referências de dados
Variáveis de referência
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.
Referências de dados 45
Passagem de parâmetros
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 function(int & x)
{
x--;
return x;
}
Temos em C++o operador & que se comporta diferentemente de C, 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 pointero. (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.
Vejamos um exemplo usando a função anterior:
Referências de dados 46
int m = 4;
function(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
#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;
system ("pause");
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;
Referências de dados 47
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;
system ("pause");
return 0;
}
Exemplo: Swap
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;
system ("pause");
return 0;
}
Referências de dados 48
Comparação entre passagem por referência e ponteiros
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;
doubleIt(num); //chamo função, passando parametro num
cout << "The number doubled in main is " << num << endl;
system ("pause");
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;
doubleIt(&num);//passei parametro como endereço
cout << "The number doubled in main is " << num << endl;
system (pause);
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); // by reference
doubleIt(&num); // by address
•• No prototype da função (confirmar o ponto e virgula)
void doubleIt(int&); // by reference
void doubleIt(int*); // by address
•• Na function header
void doubleIt (int& x) // by reference
void doubleIt (int* x) // by address
•• dentro do body function (dentro da própria função)
x
*x
Podemos pensar que passando por referência parece ser muito mais simples do que passado por address.
Na verdade existem certas funções da biblioteca que só permitem a passagem por address.
Entrada e saída de dados 49
Entrada e saída de dados
Entrada e saída
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 iosteam, 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
Paraentendermos 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.
Entrada e saída de dados 50
cout
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
A 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 (?)
Entrada e saída de dados 51
• \\ barra oposta (contrabarra) (\)
cin
O objecto cin obtém informação do "standard input" (que usualmente é o teclado). Este objecto 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 extracçã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.
Entrada e saída de dados 52
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
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');
#ifdefWIN32
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 imprimivel;
• 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.
Entrada e saída de dados 53
A função cin.get()
Já tivemos oportunidade para discutir a função getline (função membro) do objecto 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 extracçã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 objecto 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.
Entrada e saída de dados 54
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()
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 objecto 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 é exactamente 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…
Entrada e saída de dados 55
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()
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);
Entrada e saída de dados 56
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 permanenece o caractere nova linha 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.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.3. Verificar se temos o caractere nova linha no "input buffer" depois de utilizar o get(), se tivermos deveremos
utilizar o ignore().
Entrada e saída de dados 57
• 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
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 é á 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 e saída de dados 2 58
Entrada e saída de dados 2
Entrada/Saída em ficheiros (arquivos)
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)
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)?
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
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 no 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.
Entrada e saída de dados 2 59
biblioteca padrão fstream
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)
Um ficheiro(arquivo) deve ser aberto pelo programa para que o mesmo possa ser manipulado, a abertura do arquivo
implica, entre outras coisas, em 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.1. Usando um construtor;
2.2. Usando a função membro chamada de "open".
Usando o Construtor
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"
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:
Entrada e saída de dados 2 60
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)
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.
Entrada e saída de dados 2 61
Abrir um arquivo para leitura
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.
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;
Entrada e saída de dados 2 62
}
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.
Entrada e saída de dados 2 63
Fechar um ficheiro (arquivo)
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;
Entrada e saída de dados 2 64
}
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
Entrada e saída de dados 2 65
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;
Entrada e saída de dados 2 66
getline(infile, data);
cout << data << endl;
infile.close();
}
#ifdef WIN32
system ("pause");
#endif
return 0;
}
Looping pelo ficheiro (arquivo).
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())
Entrada e saída de dados 2 67
{
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.2. . readFile - ler do ficheiro (arquivo) usando o ifstream
3.3. . Cada função irá verificar se o ficheiro (arquivo) foi aberto com sucesso
#include <fstream>
#include <iostream>
#include <string>
using namespace std;
Entrada e saída de dados 2 68
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;
Entrada e saída de dados 2 69
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
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.
Entrada e saída de dados 2 70
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
•• 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
•• 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
•• 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;
Entrada e saída de dados 2 71
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
1.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.2. . Quero que seja a pessoa a escolher o nome do ficheiro (arquivo) e escrever também a localização;
3.3. . Quero que seja depois tansformado num sistema modular;
4.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;
Entrada e saída de dados 2 72
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;
}
Manipulando strings 73
Manipulando strings
"Char strings" e Strings
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 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0x0A
Dado U m a f r a s e .
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;
Manipulando strings 74
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.
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.
Manipulando strings 75
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++
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.1. Podemos definir um objeto string vazio, para futuramente usarmos de acordo com a necessidade;
2.2. Podemos criar um objeto string com uma cópia de outro;
3.3. Podemos criar um objeto string com uma cópia de uma porção de outra string;
4.4. Podemos criar um objeto string com uma cópia de uma parte de uma "char string";
5.5. Podemos criar um objeto string com uma cópia de uma "char string";
6.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.1. Atribuir o valor de uma string para outra;
2.2. Acessar caracteres individualmente;
3.3. Adicionar uma string no final de outra;
4.4. Concatenar strings;
Manipulando strings 76
5.5. Enviar uma string a um output stream;
6.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 = string("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++
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;
}
Manipulando strings 77
Comparando formas de operar strings em C e C++
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
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
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;
Manipulando strings 78
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
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
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)
Manipulando strings 79
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 stringsPrimeira
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.1. Comparar uma "string" de entrada (str2) com o conteúdo do objeto a qual ela pertence;
2.2. Comparar uma "C-string" apontada por um ponteiro com o conteúdo do objeto a qual ela pertence;
3.3. Comparar uma seção começando em (pos1) do objeto, a qual contém (n1) caracteres, com a "string" de entrada
(str2);
4.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.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.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
==.
Manipulando strings 80
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
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;
Manipulando strings 81
#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 ">>".
Classes
Esta página precisa ser reciclada (discuta).
Ao melhorá-la, você estará ajudando o Wikilivros.
Classes
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.
Classes 82
Origem (atributos)
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)
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.
Classes 83
Conceituação
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 taisoperaçõ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
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>
#ifndef WIN32 //No caso de não estar programando em Windows
#include <conio.h> //será incluída a biblioteca conio.h
#endif //que possibilita usar getch(); em vez de system("PAUSE");
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);
#ifdef WIN32
system ("pause");
#else
getch();
#endif
return 0;
}
void setValues(Person& pers)
{
cout << "Informe o nome da pessoa: ";
getline(cin, pers.name);
Classes 84
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);
};
Classes 85
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);
}
Classes 86
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
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);
Classes 87
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 operadorde 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)
Classes 88
{
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();
system (“pause”);
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();
system ("pause");
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.
Classes 89
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.
Classes 90
Definição de classes
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);
};
Classes 91
Especificadores de acesso
• 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.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
Conceito
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;
Classes92
•• 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
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.
Classes 93
Destrutores
Conceito
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
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.
Classes 94
"copy constructors"
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>
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:
Classes 95
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:
#include <iostream>
using namespace std;
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
Classes 96
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.
Encapsulamento
Conceito
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
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 atributooferece 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)
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;
...
...
};
Encapsulamento 97
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
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" )
Encapsulamento 98
{ 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
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()
Encapsulamento 99
{ cout << "Função de imprimir no Espaco alheio" << endl;
}
}
Herança
Conceito
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
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>
Herança 100
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";
}
Herança 101
}
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
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
Herança 102
• 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.1. private
2.2. protected
3.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:
Herança 103
#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
Herança 104
{
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");
#endifreturn 0;
}
Agora já não podemos acessar as funções porque estão privadas.
Heranças múltiplas
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
Herança 105
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
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; }
};
Herança 106
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"; }
Herança 107
~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
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
{
Herança 108
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;
Herança 109
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(); // mostra4 3 5
#ifdef WIN32
system ("pause");
#endif
return 0;
}
Superposição de funções
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
Herança 110
{
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.
Herança 111
Acessando funções superpostas da classe base
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;
}
Polimorfismo 112
Polimorfismo
Esta página precisa ser reciclada (discuta).
Ao melhorá-la, você estará ajudando o Wikilivros.
Conceito
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
#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";
Polimorfismo 113
}
};
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;
}
Classe base virtual
Consideremos o seguinte programa:
// Este programa comnté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;
};
class derived3 : public derived1, public derived2 /* "derived3" herda
características de "derived1" e "derived2". Isto significa que há duas
cópias da base em "derived3"! */
{
Polimorfismo 114
public:
int sum;
};
int main()
{
derived3 ob;
ob.i = 10; // Isto se torna abí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 derived 2 são herdadas como classes base. A classe derived3 herda quer derived 1 e derived2.
como resultado temos 2 cópias da classe base presentes no objecto da derived 3. 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;
};
Polimorfismo 115
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 objectos 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:
Polimorfismo 116
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
Friend 117
Friend
Esta página precisa ser reciclada (discuta).
Ao melhorá-la, você estará ajudando o Wikilivros.
O que é
Friend é um atributo especial de acesso a classes. Com ele declaramos que uma função fora de uma classe é amiga da
mesma.
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"
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);
}
Friend 118
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
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 119
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.
Classes internas
Conceituação
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);
Classes internas 120
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çãodo
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.
Sobrecarga de operadores
Modificando operadores
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
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.
Sobrecarga de operadores 121
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 traz 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 122
Alocação dinâmica de memória
Esta página precisa ser reciclada (discuta).
Ao melhorá-la, você estará ajudando o Wikilivros.
Alocação dinâmica de memória
O compilador reserva espaço na memória para todos os dados declarados explicitamente, mas se usarmos ponteiros
precisamos reservar o espaço necessário e colocar o endereço inicial nos mesmos. 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.
Refere-se à possibilidade de termos o nosso programa a correr e o utilizador ter de inserir dados e como tal não
sabemos exatamente a quantidade de dados que o utilizador vai colocar, portanto temos de arranjar uma memória
que nos permita lidar com esta indeterminação quanto à quantidade de dados inseridos.
Este é o caso em que não sabemos no momento da programação a quantidade de dados que deverão ser inseridos
mas o programa já está a correr. É tentar responder a perguntas: quantas pessoas existem na tua turma? Quantas
letras vamos escrever, etc. Em vez de estarmos a prever um limite superior para abarcar todas as situações, temos
esta possibilidade do dinâmico. Além de que colocar no momento da programação cria reserva de memória por isso,
estaríamos a reservar memória para um limite que possivelmente não iríamos ter necessidade. O exemplo típico disto
é os processadores de texto. em que não sabemos a quantidade de letras que o utilizador vai escrever.
Vamos voltar a uma ponta solta num dos capítulos anteriores, onde queríamos fazer com que o utilizador dissesse
quantos elementos do array é que se deveria utilizar. Já dissemos antes que o declarador do nº de elementos do array
tem de ser ou uma constante ou um literal, mas não pode ser uma variável. Isso dá erro. Aqui vai o exemplo desse
erro:
#include <iostream>
using namespace std;
int main ()
{
int numTests;
cout << "digite o numero de testes : ";
cin >> numTests;
int testScore[numTests];
system ("pause");
return 0;
}
A razão da exigência de ter um constant (ou literal) é que vamos alocar memória para o array na altura da
compilação, e o compilador necessita de saber exactamente a quantidade de memória que deve reservar… porém se
a variável é o size declarator, o compilador não sabe quanta memória deve reservar para alocar a variável, pois o seu
valor pode mudar.
Alocação dinâmica de memória 123
Operador new
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++)
{
cout << "Enter test score #" << i + 1 << " : ";
cin >> iPtr[i];
}
for (int i = 0; i < numTests; i++)
cout << "Test score #" << i + 1 << " is "<< iPtr[i] << endl;
delete [] iPtr;
system ("pause");
return 0;
}
Ou seja conseguimos criar um array onde é o utilizador a definir o tamanho do array e que depois coloca o valor para
cada um dos elementos.
O operador new retorna o endereço onde começa o bloco de memória. e como retorna um endereço vamos colocá-lo
num pointer.
Necessitamos do uso do pointer que deverá ser do mesmo tipo que o tipo de variável que é alocado dinamicamente.
int * iPtr = new int[numTests];
•• Temos termo NEW. Que é um operador cuja função é alocar dinamicamente memória
•• Temos o tipo da variável alocada dinamicamente
•• Repare que NÃO temos o nome do array
•• Uma vez que o array fica sem nome para nos referirmos a cada elemento do array teremos de usar o pointer.
Podemos inicializar de duas maneiras:
int *IDpt = new int;
*IDpt = 5;
ou
int *IDpt = new int(5); //Allocates an int object and initializes
it to value 5.
char *letter = new char('J');Alocação dinâmica de memória 124
Operador Delete - Memory Leak
O tempo de vida de uma variável criada dinamicamente é o tempo de execução do programa. Se um ponteiro aponta
para uma variável dinâmica e fica out of scope, já não conseguiremos acessar essa memória criada dinamicamente.
Fica indisponível. A isso se chama Memory Leak
Explicando: 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 chamar essa
memória porque ela não tem nome! Apenas tínhamos o endereço que estava no ponteiro.
Portanto, se realmente não necessitamos mais dos dados que estão nessa memória dinâmica, em vez de eles estarem
a ocupar espaço vamos apagá-los! Necessitamos de libertar essa memória através do operador delete – este operador
entrega ao sistema operativo a memoria reservada dinamicamente.
A sintaxe é
delete [] iPtr;
Este delete operator não apaga o ponteiro mas sim a memória onde o ponteiro aponta.
dynamic memory allocation funciona porque a memória não é reservada no momento da compilação, mas antes na
execução. Em vez de ser no STACK (compilação) a memória é reservada no HEAP (execução). O heap é uma parte
da memória que é usada como memória temporária.
Pergunta: onde é que fica situado o Heap?
Vamos explicar melhor todo este processo: pois isto tem de entrar na cabeça!!!
void myfunction()
{
int *pt;
int av;
pt = new int(1024);
....
....
//No delete
}
int main()
{
while (some condition exists) // Pseudo-code
{
myfunction();
}
exit 0;
}
quando a função “myfunction” é chamada a variável “av” é criada no stack e quando a função acaba a variável é
retirada do stack. 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 é retirado do stack. Porém o objeto alocado dinamicamente ainda existe.
E agora não conseguimos apagar esse objeto por que ele não tem nome e a única maneira que tínhamos para saber
onde ele estava era através do ponteiro que terminou quando termina a função pois ele é local.
Então à medida que o programa continua a operar mais e mais memória será perdida do Heap (free store). se o
programa continuar o tempo suficiente, deixaremos de ter memória disponível e o programa deixará de operar.
Alocação dinâmica de memória 125
Retornando um ponteiro para uma variável local
#include <iostream>
using namespace std;
char * setName();
int main (void)
{
char* str = setName(); //ponteiros para a função
cout << str; //imprimo o valor do ponteiros?
system (“pause”);
return 0;
}
char* setName (void)
{
char name[80];
cout << "Enter your name: ";
cin.getline (name, 80);
return name;
}
O que se passou aqui é que o ponteiro que sai da função setName aponta para o array local cuja vida acaba quando a
função termina de executar. A solução é estender o tempo de vida. Uma solução era tornar esse array global, mas
existem alternativas melhores.
Retornando um Ponteiro a uma Variável Local Estática
Uma dessas alternativas é
#include <iostream>
using namespace std;
char * setName();
int main (void)
{
char* str = setName();
cout << str;
system (“pause”);
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.
O ponteiro do setName aponta para o array local, e como foi utilizado o static, ele perdura até fim da função,
terminando apenas quando o programa acaba.
Alocação dinâmica de memória 126
Returning a Pointer to a Dynamically Created Variable
Outra alternative, talvez melhor
#include <iostream>
using namespace std;
char * setName();
int main (void)
{
char* str= setName();
cout << str;
delete [] str; //faço o delete para evitar o memory leak
system ("pause");
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. O
address do ponteiro local é atribuído no main a outro ponteiro no main –str. Depois este ponteiro é usado até ao fim
da execução do programa. Este é um exemplo onde diferentes ponteiros apontam para o mesmo endereço.
Mas ter atenção que se fizermos o delete através de um ponteiro, ter cuidado com o segundo ponteiro que aponta
para a memoria que acabou de ser deslocada.
Alocar dinamicamente Arrays
Ora o que fizemos antes com variáveis, vamos ter de fazer com arrays.
int *pt = new int[1024]; //allocates an array of 1024 ints
double *myBills = new double[10000];
/* This doesn't mean I owe 10000.0, but rather allocates an array of
10000 doubles to hold the amounts of the thousands of bills I receive
monthly. */
Notar a diferença:
int *pt = new int[1024]; //allocates an array of 1024 ints
int *pt = new int(1024); //allocates a single int with value 1024
a melhor maneira para alocar um array dinamicamente é usar o loop
int *buff = new int[1024];
for (i = 0; i < 1024; i++)
{
*buff = 52; //Assigns 52 to each element;
buff++;
Alocação dinâmica de memória 127
}
ou se quisermos desta maneira
int *buff = new int[1024];
for (i = 0; i < 1024; i++)
{
buff[i] = 52; //Assigns 52 to each element;
}
para utilizar o delete em arrays
delete[] pt;
delete[] myBills;
Dangling Pointers
int *myPointer;
myPointer = new int(10);
cout << "The value of myPointer is " << *myPointer << endl;
delete myPointer;
*myPointer = 5;
cout << "The value of myPointer is " << *myPointer << endl;
neste exemplo libertámos a memória dinâmica, mas o ponteiro continua isto é um bug tremendo, e muito difícil de
detectar. o programa continua a correr e a secção de memória pode ser usada por outro objeto dinâmico. acontece
que essa memoria estará corrompida se continuar a usar o myPointer. a melhor maneira é depois do delete fazer
apontar para zero, fazê-lo um ponteiro nulo. se tentarem usar o ponteiro iremos ter a run time exception e o bug pode
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; //This statement will cause an run-time exception,
now.
cout << "The value of myPointer is " << *myPointer << endl;
Verificar a existência de memória para dinâmica
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]; // if it fails an exception is thrown
“bad_alloc” exception
vamos ver este caso quase no ultimo capitulo- isto é uma capítulo avançado
•• notthrow, aqui no caso de não se conseguir a memória retorna um ponteiro nulo, e o programa continua.
bobby = new (nothrow) int [5];
Alocação dinâmica de memória 128
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 << "How many numbers would you like to type? ";
cin >> i;
p= new (nothrow) int[i]; //criámos I variaveis na execuçãoif (p == 0)
cout << "Error: memory could not be allocated";
else
{
for (n=0; n<i; n++)
{
cout << "Enter number: ";
cin >> p[n];
}
cout << "You have entered: ";
for (n=0; n<i; n++)
cout << p[n] << ", ";
delete[] p;
}
system ("pause");
return 0;
}
Exceções 129
Exceções
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.
Namespace
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.
Templates
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 usadas 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
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;
Templates 130
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 acto 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';
Templates 131
}
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.
Explicitly overloading a generic function
apesar de uma função genérica poder ser overload automaticamente se necessário, nós podemos explicitar. a isso
chamamos deexplicit specialization
Compilação
Esta página precisa ser reciclada (discuta).
Ao melhorá-la, você estará ajudando o Wikilivros.
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
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.1. Preprocessor - pré-processador
2.2. Compiler - compilador
3.3. Linker
Pré-processador
Este é um programa que busca no código fonte – no código que escrevemos – por directivas 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
1. 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]
Compilação 132
Compilador
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 ficheiro á 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 própria á linguagem. 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
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
operativo, caso isso não aconteça teremos de fazer o dowload 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
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 souce 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 compiler 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çãoseparada, 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 loades 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 colecçã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 ums ficheiros com a terminação “.exe”. Só assim é que o sistema
operativo reconhece o programa como independente.
Os compiladores actuais incluem também pré-compiladores, (ou pré-processadores) (antes eram software
independente, extra), estes precompiladores 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.
---
Compilação 133
Referências
[1] http:/ / www. dca. fee. unicamp. br/ cursos/ EA876/ apostila/ HTML/ node150. html
O pré-processador C
Lista de Palavras Reservadas do C++
Estas são as palavras reservadas do c++:
asm auto break case
catch char class const
continue default delete do
double else enum extern
float for friend goto
if inline int long
new operator private protected
public register return short
signed sizeof static struct
switch template this throw
try typedef union unsigned
virtual void volatile while
Lista de Sequências de Escape
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++
Tabela ASCII 134
Tabela ASCII
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 é somente um esboço.
Ampliando-a você ajudará a melhorar o Wikilivros.
Programa 1
#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
#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++)
Tabela ASCII 135
{
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;
}
C++11
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:
•• std::array
•• std::tuple
•• std::foward_list
•• std::unordered_set
•• std::unordered_multiset
•• std::unordered_map
•• std::unordered_multimap
• Regular expressions library: http:/ / en. cppreference. com/ w/ cpp/ regex
• Atomic operations library: http:/ / en. cppreference. com/ w/ cpp/ atomic
• Thread support library: http:/ / en. cppreference. com/ w/ cpp/ thread
Fontes e Editores da Página 136
Fontes e Editores da Página
Objetivo Fonte: http://pt.wikibooks.org/w/index.php?oldid=212683 Contribuidores: Contribuidor, Helder.wiki, Marcos Antônio Nunes de Moura, SallesNeto BR, Wbrito
Por que C++? Fonte: http://pt.wikibooks.org/w/index.php?oldid=244119 Contribuidores: Abacaxi, Helder.wiki, Jorge Morais, Marcos Antônio Nunes de Moura, SallesNeto BR, Wbrito, 2
edições anónimas
Diferenças entre C e C++ Fonte: http://pt.wikibooks.org/w/index.php?oldid=244120 Contribuidores: Abacaxi, Fabio Basso, Helder.wiki, Marcos Antônio Nunes de Moura, Master, 1 edições
anónimas
Introdução Fonte: http://pt.wikibooks.org/w/index.php?oldid=262200 Contribuidores: Abacaxi, Helder.wiki, Mappim, Marcos Antônio Nunes de Moura, Rautopia, 4 edições anónimas
Alô, Mundo! Fonte: http://pt.wikibooks.org/w/index.php?oldid=255607 Contribuidores: Abacaxi, Albmont, Carlosmacapuna, Daveiro, Edudobay, Focli, Helder.wiki, Jorge Morais,
Marcelo-Silva, Marcos Antônio Nunes de Moura, Petrusz1, Rautopia, Renatofq, Sygmn, Wbrito, 20 edições anónimas
Variáveis e constantes Fonte: http://pt.wikibooks.org/w/index.php?oldid=238714 Contribuidores: Jorge Morais, Marcos Antônio Nunes de Moura, Rautopia
Ponteiros Fonte: http://pt.wikibooks.org/w/index.php?oldid=247694 Contribuidores: Abacaxi, Albmont, Jonathan Queiroz, Jorge Morais, Marcos Antônio Nunes de Moura, 3 edições anónimas
Vetores Fonte: http://pt.wikibooks.org/w/index.php?oldid=255610 Contribuidores: Abacaxi, Daveiro, Edudobay, Jorge Morais, Marcelo-Silva, Marcos Antônio Nunes de Moura, Master,
Raylton P. Sousa, Wbrito, 4 edições anónimas
Estruturas Fonte: http://pt.wikibooks.org/w/index.php?oldid=256735 Contribuidores: Abacaxi, Albmont, Marcos Antônio Nunes de Moura, 5 edições anónimas
Operadores Fonte: http://pt.wikibooks.org/w/index.php?oldid=244121 Contribuidores: Abacaxi, Marcos Antônio Nunes de Moura
Decisão e controle de fluxo Fonte: http://pt.wikibooks.org/w/index.php?oldid=263750 Contribuidores: Marcos Antônio Nunes de Moura, 11 edições anónimasLaços Fonte: http://pt.wikibooks.org/w/index.php?oldid=260602 Contribuidores: Diegobza, Helder.wiki, LlamaAl, Lucas Daltro, Marcos Antônio Nunes de Moura, 1 edições anónimas
Funções Fonte: http://pt.wikibooks.org/w/index.php?oldid=249232 Contribuidores: Abacaxi, Jonas AGX, 2 edições anónimas
Referências de dados Fonte: http://pt.wikibooks.org/w/index.php?oldid=239660 Contribuidores: Albmont, Marcos Antônio Nunes de Moura, Reder, 5 edições anónimas
Entrada e saída de dados Fonte: http://pt.wikibooks.org/w/index.php?oldid=255608 Contribuidores: Abacaxi, Albmont, Daveiro, Edudobay, Jeferson90, Jonathan Queiroz, Jorge Morais,
Marcelo-Silva, Marcos Antônio Nunes de Moura, Wbrito, 15 edições anónimas
Entrada e saída de dados 2 Fonte: http://pt.wikibooks.org/w/index.php?oldid=256748 Contribuidores: Abacaxi, Daveiro, Diego.g.a, Edudobay, Jorge Morais, Marcelo-Silva, Marcos Antônio
Nunes de Moura, Raylton P. Sousa, Wbrito, 19 edições anónimas
Manipulando strings Fonte: http://pt.wikibooks.org/w/index.php?oldid=264914 Contribuidores: Abacaxi, Albmont, Daveiro, Fabiobasso, Jorge Morais, Marcelo-Silva, Marcos Antônio Nunes
de Moura, Wbrito, 20 edições anónimas
Classes Fonte: http://pt.wikibooks.org/w/index.php?oldid=264082 Contribuidores: Abacaxi, Albmont, Daveiro, Jeferson90, Jorge Morais, Marcelo-Silva, Marcos Antônio Nunes de Moura,
Wbrito, 17 edições anónimas
Encapsulamento Fonte: http://pt.wikibooks.org/w/index.php?oldid=214125 Contribuidores: MGFE Júnior, Marcos Antônio Nunes de Moura, 2 edições anónimas
Herança Fonte: http://pt.wikibooks.org/w/index.php?oldid=214299 Contribuidores: Albmont, Daveiro, Jorge Morais, Marcelo-Silva, Marcos Antônio Nunes de Moura, Wbrito, 14 edições
anónimas
Polimorfismo Fonte: http://pt.wikibooks.org/w/index.php?oldid=254553 Contribuidores: Abacaxi, Daveiro, Jorge Morais, Marcelo-Silva, Marcos Antônio Nunes de Moura, Wbrito, 5 edições
anónimas
Friend Fonte: http://pt.wikibooks.org/w/index.php?oldid=255619 Contribuidores: Abacaxi, Daveiro, Jorge Morais, Marcelo-Silva, Marcos Antônio Nunes de Moura, Wbrito, 8 edições
anónimas
Classes internas Fonte: http://pt.wikibooks.org/w/index.php?oldid=212708 Contribuidores: Marcos Antônio Nunes de Moura
Sobrecarga de operadores Fonte: http://pt.wikibooks.org/w/index.php?oldid=231778 Contribuidores: Marcos Antônio Nunes de Moura, Raylton P. Sousa, 5 edições anónimas
Alocação dinâmica de memória Fonte: http://pt.wikibooks.org/w/index.php?oldid=255618 Contribuidores: Abacaxi, Daveiro, Helder.wiki, Jorge Morais, Marcelo-Silva, Marcos Antônio
Nunes de Moura, Petrusz1, Wbrito, 14 edições anónimas
Exceções Fonte: http://pt.wikibooks.org/w/index.php?oldid=203880 Contribuidores: Daveiro, Jorge Morais, Marcelo-Silva, Marcos Antônio Nunes de Moura, SallesNeto BR, Wbrito, 5 edições
anónimas
Namespace Fonte: http://pt.wikibooks.org/w/index.php?oldid=203888 Contribuidores: Daveiro, Jorge Morais, Marcelo-Silva, Marcos Antônio Nunes de Moura, Wbrito, 4 edições anónimas
Templates Fonte: http://pt.wikibooks.org/w/index.php?oldid=255613 Contribuidores: Abacaxi, Daveiro, Jorge Morais, Marcelo-Silva, Marcos Antônio Nunes de Moura, Wbrito, 8 edições
anónimas
Compilação Fonte: http://pt.wikibooks.org/w/index.php?oldid=222113 Contribuidores: Daveiro, Edudobay, Jonas AGX, Jorge Morais, Marcelo-Silva, Marcos Antônio Nunes de Moura,
Master, Renatofq, SallesNeto BR, Wbrito, 5 edições anónimas
Lista de Palavras Reservadas do C++ Fonte: http://pt.wikibooks.org/w/index.php?oldid=238733 Contribuidores: Lucas Daltro, 1 edições anónimas
Lista de Sequências de Escape Fonte: http://pt.wikibooks.org/w/index.php?oldid=253634 Contribuidores: Abacaxi, Yoroi
Tabela ASCII Fonte: http://pt.wikibooks.org/w/index.php?oldid=247032 Contribuidores: Abacaxi, Helder.wiki, 2 edições anónimas
C++11 Fonte: http://pt.wikibooks.org/w/index.php?oldid=258953 Contribuidores: 1 edições anónimas
Fontes, Licenças e Editores da Imagem 137
Fontes, Licenças e Editores da Imagem
Imagem:Rekopis chopin.jpg Fonte: http://pt.wikibooks.org/w/index.php?title=Ficheiro:Rekopis_chopin.jpg Licença: Public Domain Contribuidores: Andreagrossmann, Kevyn, Maire, Man
vyi, Niki K, Subitosera, Tsca
Image:Recycle001.svg Fonte: http://pt.wikibooks.org/w/index.php?title=Ficheiro:Recycle001.svg Licença: desconhecido Contribuidores: Users Cbuckley, Jpowell on en.wikipedia
Licença 138
Licença
Creative Commons Attribution-Share Alike 3.0
//creativecommons.org/licenses/by-sa/3.0/