Página em branco
Walter J. Savitch
Tradução Claudia Martins
Revisão Técnica Oswaldo Ortiz Fernandes Jr. Professor concursado em Teoria da Computação e Compiladores no Centro Universitário Municipal de SC do Sul Bacharel e Licenciado em Física pela USP Pós-graduado em Física pela USP Mestrando em Engenharia Eletrônica e Computação pelo ITA
ASSOCIAÇÃO BRASILEIRA DE DIREITOS REPROGRÁFICOS
2004 by Pearson Education do Brasil Título original: Absolute C++ — first edition © 2002 by Pearson Education, Inc. ©
Publicação autorizada a partir da edição original em inglês publicada pela Pearson Education, Inc., sob o selo Addison Wesley Todos os direitos reservados. Nenhuma parte desta publicação poderá ser reproduzida ou transmitida de qualquer modo ou por qualquer outro meio, eletrônico ou mecânico, incluindo fotocópia, gravação ou qualquer outro tipo de sistema de armazenamento e transmissão de informação, sem prévia autorização, por escrito, da Pearson Education do Brasil.
Diretor Editorial: José Martins Braga Editor: Roger Trimer Editora de Texto: Adriane Gozzo Preparação: Sandra Garcia Revisão: Nilma Guimarães Designer de Capa: Marcelo Françozo , sobre o projeto original de Leslie Haimes, com foto de Renee Lynn/Stone by Getty Images Editoração Eletrônica: ERJ Composição Editorial e Artes Gráficas Ltda.
Dados Internacionais de Catalogação na Publicação (CIP) (Câmara Brasileira do Livro, SP, Brasil) Savitch, Walter J. C++ absoluto / Walter Savitch ; tradução Claudia Martins ; revisão técnica Oswaldo Ortiz Fernandes Jr. -- São Paulo : Addison Wesley, 2004.
ISBN: 85-88639-09-2
1. C++ (Linguagem de programação para computad ores) I. Título.
03 03-2860
CDD-005.133
Índices para catálogo sistemático 1. C++ C++ : Linguagem de programação programação : Computadores : Processamento de dados 005.133
2004 Direitos exclusivos para a língua portuguesa cedidos à Pearson Education do Brasil, uma empresa do grupo Pearson Education Av. Ermano Marchetti, Marchetti, 1435 CEP: 05038-001, Lapa – São Paulo – SP Tel: (11) 3613-1222 Fax: (11) 3611-0444 e-mail:
[email protected]
Prefácio C++ Absoluto
foi projetado como um manual e livro de referência para a programação na linguagem C++. Embora inclua técnicas de programação, foi organizado mais em função dos recursos da linguagem C++ do que de algum currículo específico de técnicas de programação. O público que eu tinha em mente ao escrevê-lo era o de estudantes universitários, especialmente de ciências da computação, ainda sem muita experiência em programação com a linguagem C++. Este livro foi projetado para ser útil a um grande número de usuários. Os capítulos iniciais foram escritos em um nível acessível a iniciantes, embora os quadros desses capítulos sirvam para apresentar rapidamente a sintaxe básica do C++ a programadores mais experientes. Os últimos capítulos também são acessíveis, mas foram escritos em um nível adequado a estudantes que já evoluíram para tópicos mais avançados. Este livro também inclui uma introdução aos padrões e à Linguagem Unificada de Modelagem (UML) e um capítulo inteiro sobre Recursão.
RECURSOS ESPECIAIS P ADRÕES ANSI/ISO C++ Este livro foi escrito de acordo com os novos padrões ANSI/ISO C++.
STANDARD TEMPLATE LIBRARY A Standard Template Library é uma extensa coleção de bibliotecas de classes de estrutura estrutura de dados pré-programapré-programadas e algoritmos importantes. A STL talvez seja um tópico tão extenso quanto o núcleo da linguagem C++. Este livro contém uma sólida introdução à STL. Há um capítulo inteiro sobre templates e outro sobre as particularidades da STL, além de outros assuntos relacionados com a STL em capítulos diversos.
PROGRAMAÇÃO ORIENTADA A OBJETOS Este livro trata da estrutura da linguagem C++. Dessa forma, os primeiros capítulos, que abordam aspectos do C++ comuns a quase todas as linguagens de programação de alto nível, não estão direcionados especificamente à programação orientada a objetos (OOP). Isso faz sentido em se tratando de um livro de referência e para ensino de uma segunda linguagem. Entretanto, considero C++ uma linguagem OOP. Se você estiver programando realmente em C++ e não em C, precisa tirar proveito dos recursos OOP do C++. Este livro fornece uma extensa abordagem sobre encapsulamento, herança e polimorfismo como entendidos na linguagem C++. O capítulo final sobre padrões e UML apresenta outros assuntos relacionados à OOP.
FLEXIBILIDADE NA ORDENAÇÃO DOS TÓPICOS C++ Absoluto permite
aos professores uma grande liberdade de reordenação do material. Isso é importante para uma obra de referência e combina com a minha filosofia de escrever livros que se adaptem ao estilo do professor em vez de amarrá-lo à preferência pessoal de ordenamento de tópicos do autor. Tendo isso em mente, a introdução de cada capítulo explica que material deve ser estudado antes que se execute cada seção do capítulo.
ACESSÍVEL AOS ESTUDANTES Não é suficiente que um livro apresente os tópicos certos na ordem certa. Nem é suficiente que seja claro e correto quando lido por um professor ou outro especialista. O material precisa ser apresentado em uma forma acessível a quem ainda não o conhece. Como meus outros manuais, que se revelaram bastante populares entre os estudantes, este livro foi redigido de maneira amigável e acessível.
Quadros Todos os pontos principais são resumidos em quadros, espalhados ao longo de cada capítulo, que servem como resumos do conteúdo, como fonte de referência rápida e como forma de aprender rapidamente a sintaxe do C++
VI
Prefácio
para recursos que o leitor já conhece de forma geral, mas para os quais necessita saber os detalhes do emprego da linguagem C++. Exercícios de Autoteste
Cada capítulo contém diversos Exercícios de Autoteste em pontos estratégicos. As respostas completas para todos os exercícios são dadas ao final de cada capítulo. Outros Recursos
Seções de "armadilhas", de técnicas de programação e exemplos de programas completos com amostras E/S são dadas ao longo de cada capítulo, que termina com uma seção de resumo e vários projetos de programação adequados para serem atribuídos aos estudantes. M ATERIAL
DE
A POIO POIO
Este livro foi planejado para uso com o Microsoft Visual C++. No site do livro em www.aw.com/savitch_br você encontra links para diversos sites relacionados, além dos seguintes recursos: ■ ■
Código-fonte do livro Transparências em PowerPoint
Os seguintes recursos estão disponíveis somente para os professores que adotam o livro. Por favor, entre em contato com o seu representante de vendas local ou envie um e-mail para
[email protected] para ter acesso ao: ■
Manual do professor (em inglês)
A GRADECIMENTOS GRADECIMENTOS
Diversas pessoas contribuíram de forma inestimável para tornar este livro uma realidade. Frank Ruggirello e minha editora Susan Hartman, da Addison-Wesley, foram os primeiros a imaginarem esta obra. Susan Hartman, Galia Shokry, Lisa Kalner e outras pessoas fantásticas da Addison-Wesley foram uma contínua fonte de apoio e encora jamento para a revisão técnica, revisão de provas e publicação. Cindy Kogut fez um incrível trabalho de edição de texto. Sally Boylan e outros da Argosy Publishing fizeram um ótimo trabalho, efetuado em um curto espaço de tempo, na digitalização das páginas. David Teague merece um agradecimento especial. Apreciei muito seu trabalho árduo, suas ótimas sugestões e a pesquisa cuidadosa para este livro. Agradeço a meu bom amigo Mario Lopez pelas muitas conversas proveitosas que tivemos sobre o C++. Os seguintes revisores forneceram correções e sugestões que contribuíram imensamente para o produto final. Agradeço a todos. Em ordem aleatória, eles são: Kenrick Mock, University of Alaska, Anchorage; Richard Albright, University of Delaware; H. E. Dunsmore, Purdue University; Christopher E. Cramer; Drue Coles, Boston University; Evan Golub, University of Maryland; Stephen Corbesero, Moravian College; Fredrick H. Colclough, Colorado Technical University; Joel Weinstein, Northeastern University; Stephen P. Leach, Florida State University; Alvin S. Lim, Auburn University; e Martin Dulberg, North Carolina State University. Mais uma vez, agradeço a David Teague, desta vez pelo seu excelente trabalho na preparação do manual do professor. Finalmente, agradeço a Christina por ter sido companheira quando eu ficava trabalhando até tarde no livro e por haver me encorajado em vez de reclamar. Walter Savitch http://www-cse.ucsd.edu/users/savitch/
[email protected]
Sumário Capí Capítu tulo lo 1 1.1 1.2 1.3 1.3 1.4 1.5 1.5 Capí Capítu tulo lo 2 2.1 2.1 2.2 2.2 2.3 Capí Capítu tulo lo 3 3.1 3.1 3.2 3.2 3.3 Capít Capítul ulo o4 4.1 4.2 4.2 4.3 4.3 Capítu ítulo 5 5.1 5.1 5.2 5.2 5.3 5.3 5.4 5.4 Capí Capítu tulo lo 6 6.1 6.2 Capít Capítul ulo o7 7.1 7.2 7.3
Fun Fundame dament ntos os do C++ C++
1
Int Introdução ção ao C++ 1 Variá Variáve veis is,, Expre Expressõ ssões es e Decla Declaraç rações ões de Atrib Atribuiç uição ão Entr Entrad ada/ a/Sa Saíd ídaa de Term Termin inaal 18 Estilo de Programa 23 Bib Bibliot liotec ecas as e Name Namesp spac aces es 24 Flux Fluxo o de Co Cont ntro role le
4
29
Exp Expressões ões Boole ooleaanas nas 29 Est Estrut ruturas uras de Co Cont ntro rolle 35 Loops 43 Fund Fundam amen ento toss das das Funç Funçõe õess
61
Funç Funçõe õess Pred redefin finidas 61 Funç Funçõe õess Defi Defini nida dass pelo pelo Prog Progra rama mado dorr Regr egras de Esc Escopo 79 Parâ Parâme metr tros os e Sobr Sobrec ecar arga ga
69
91
Parâmetros 91 Sobr Sobrec ecar arga ga e Argu Argume men ntostos-Pa Padr drão ão 103 103 Test Testan ando do e Depu Depura ran ndo Funç Funçõe õess 110 110 Vetores
117
Intr Introd oduç ução ão aos Veto Vetore ress 117 117 Vet Vetore ores em Funç Funçõe õess 123 Prog Progra rama mand ndoo com com Veto Vetore ress 132 132 Vet Vetores ores Multi ultidi dime mens nsio iona nais is 139 139 Estru strutu tura rass e Clas Classe sess
153 153
Estruturas 15 153 Classes 16 162 Const Construt rutor ores es e Outra Outrass Ferra Ferrame menta ntass
177
Construtores 17 177 Mais ais Ferr errament entas 191 Vecto Vectors rs — Intro Introduç dução ão à Standa Standard rd Temp Templat latee Librar Libraryy
200 200
VIII
Sumário
Capítulo Capítulo 8
Sobreca Sobrecarga rga de Operador Operador,, Amigos Amigos e Referên Referências cias
207
8.1 Fundam Fundamen ento toss da Sobr Sobreca ecarga rga de Opera Operador dor 207 207 8.2 Funções Funções Amigas Amigas e Conversão Conversão de Tipo Automáti Automática ca 218 8.3 Referênc Referências ias e Mais Mais Operador Operadores es Sobrecar Sobrecarregad regados os 223 Capí Capítu tulo lo 9
Stri String ngss
241 241
9.1 9.1 Tipo Tipo Veto Vetorr para para Stri Strings ngs 241 241 9.2 Ferramen Ferramentas tas de Manipula Manipulação ção de Caracter Caracteres es 249 9.3 Clas lasse-P se-Pad adrrão string 258 Capítu Capítulo lo 10
Pontei Ponteiros ros e Vet Vetor ores es Dinâm Dinâmico icoss
277 277
10.1 0.1 Ponte onteiiros 277 277 10.2 10.2 Veto Vetore ress Dinâ Dinâmi mico coss 288 288 10.3 10.3 Classe Classes, s, Ponte Ponteir iros os e Vetore Vetoress Dinâmi Dinâmicos cos 297 Capítulo Capítulo 11
Compil Compilação ação Separada Separada e Namespa Namespaces ces
313
11.1 11.1 Comp Compililaç ação ão Sepa Separa rada da 313 313 11.2 1.2 Na Nam mespa espace cess 324 324 Capítu Capítulo lo 12
12.1 12.1 12.2 12.2 12.3 12.4 12.4
E/S E/S de Arqui Arquivo vo e Strea Streams ms
343
Stre Stream amss de E/S E/S 344 344 Ferram Ferrament entas as para para E/S E/S de Strea Stream m 355 Hierarqu Hierarquias ias de Stream: Stream: Introduç Introdução ão à Herança Herança 363 Acesso Acesso Aleató Aleatório rio a Arqui Arquivos vos 369
Capítulo 13 Recursão
377
13.1 Funções void Recursivas 377 13.2 13.2 Funçõe Funçõess Recur Recursi siva vass que Retorn Retornam am um Valor Valor 386 13.3 13.3 Pens Pensand andoo Recu Recurs rsiv ivam amen ente te 390 390 Capí Capítu tulo lo 14
Heran erança ça
403 403
14.1 14.1 Fund Fundam amen ento toss da Hera Heranç nçaa 403 403 14.2 14.2 Prog Progra rama mand ndoo com com Hera Heranç nçaa 409 409 Capítu Capítulo lo 15
Poli Polimor morfi fismo smo e Fun Funçõ ções es Virtua Virtuais is
435 435
15.1 15.1 Princ Princíp ípios ios das das Funçõ Funções es Virtua Virtuais is 435 15.2 15.2 Pontei Ponteiros ros e Funçõe Funçõess Virtua Virtuais is 444 444 Capítu Capítulo lo 16
Temp Templat lates es (Gaba (Gabarit ritos) os)
455
16.1 16.1 Temp Templa late tess de Funç Função ão 455 455 16.2 16.2 Temp Templa late tess de Clas Classe se 464 464 16.3 16.3 Temp Templa late tess e Hera Heranç nçaa 472 472 Capítu Capítulo lo 17
17.1 17.1 17.2 17.2 17.3 7.3 17.4
Estr Estrutu utura rass de Dados Dados Ligada Ligadass
481
Nós Nós e List Listas as Liga Ligada dass 482 482 Aplica Aplicaçõe çõess de Lista Lista Ligada Ligada 498 498 Iter Iterad adoores res 508 508 Árv Árvores 515
Sumário
Capítu Capítulo lo 18 Trat Tratame ament ntoo de Exceç Exceções ões 529 18.1 18.1 18.2
Fundam Fundamen ento toss do Tratam Tratamen ento to de Exceç Exceções ões 530 Técnicas Técnicas de Programa Programação ção para o Tratamen Tratamento to de Exceções Exceções
Capítu Capítulo lo 19 Stand Standard ard Templ Templat atee Librar Libraryy 19.1 9.1 19.2 9.2 19.3 19.3
Iter Iterad adoores res 550 550 Cont Co ntaainers ners 559 Algo Algori ritm tmos os Gené Genéri rico coss
Capí Capítu tulo lo 20 Padr Padrõe õess e UML UML 20.1 20.2
Padrões 585 UML 593
Apêndice 1
599
Apêndice 2
600
Apêndice 3
602
Apêndice 4
603
Apêndice 5
608
Índice
609
585 585
569 569
549
543
IX
Fundamentos do C++ Capítulo 1C++ Básico Fundamentos do C++ A Máquina Analítica não tem nenhuma pretensão de criar nada. Pode fazer qual- quer coisa que saibamos como mandá-la fazer. Pode acompanhar a análise; mas não tem o poder de antecipar quaisquer relações analíticas ou verdades. Sua ocupação é nos assistir tornando disponível aquilo que já conhecemos. Ada Augusta, Condessa de Lovelace
INTRODUÇÃO Este capítulo apresenta a linguagem C++ e fornece detalhes suficientes para permitir que você lide com programas simples envolvendo expressões, atribuições e entrada e saída (E/S) de terminal. Os detalhes das atribuições e expressões são semelhantes aos da maioria de outras linguagens de alto nível. Cada linguagem possui sua sintaxe de E/S de terminal; portanto, se você você não está familiari familiarizado zado com C++, esse aspecto aspecto pode lhe parecer parecer novo e difere diferente nte..
1.1
Introdução ao C++ A linguagem é o único instrumento da ciência. Samuel Johnson
Esta seção fornece uma visão geral da linguagem de programação C++. ■ ORIGENS DA LINGUAGEM C++
Pode-se pensar nas linguagens de programação C++ como a linguagem de programação C com classes (e outros recursos modernos adicionados). A linguagem de programação C foi desenvolvida por Dennis Ritchie, dos AT&T Bell Laboratories, na década de 70. Foi usada, a princípio, para escrever e manter o sistema operacional UNIX. (Até aquela época, os programas de sistema UNIX eram escritos em linguagem assembly ou em uma linguagem chamada B, desenvolvida por Ken Thompson, o criador do UNIX.) C é uma linguagem de finalidade geral que pode ser usada para escrever qualquer tipo de programa, mas seu sucesso e popularidade estão intimamente ligados ao sistema operacional UNIX. Se você quisesse preservar seu sistema UNIX, precisava usar C. C e UNIX se deram tão bem que logo não só os programas de sistema mas quase todos os programas comerciais executados no UNIX eram escritos na linguagem C. C se tornou tão popular que versões da linguagem foram escritas para outros sistemas operacionais populares; assim, seu uso não se limitou aos computadores que utilizavam UNIX. Entretanto, apesar de sua popularidade, C não era uma linguagem isenta de problemas. A linguagem C é peculiar porque é uma linguagem de alto nível com muitos recursos de linguagem de baixo nível. C está entre os dois extremos, o de uma linguagem de nível muito alto e o de uma linguagem de baixo nível, e nisso residem tanto sua força quanto sua fraqueza. Como a linguagem (de baixo nível) assembly, os programas em linguagem
2
Fundamentos do C++
C podem manipular diretamente a memória do computador. Por outro lado, C possui recursos de uma linguagem de alto nível, o que a torna mais fácil de ler e escrever do que a linguagem assembly. Isso faz de C uma excelente escolha para escrever programas de sistema, mas para outros programas (e em certo sentido até para programas de sistema) C não é tão fácil de entender quanto outras linguagens; além disso, não possui tantas verificações automáticas quanto outras linguagens de alto nível. Para superar essas e outras desvantagens de C, Bjarne Stroustrup, dos AT&T Bell Laboratories, desenvolveu o C++ no início da década de 80. Stroustrup projetou o C++ como um C aperfeiçoado. A maior parte da linguagem C é um subconjunto da C++, e, assim, muitos programas em C também são programas em C++. (O inverso não é verdade; muitos programas em C++ não são, definitivamente, programas em C.) Ao contrário de C, C++ possui recursos para classes e, portanto, pode ser usada para a programação orientada a objetos. ■
C++ E PROGRAMAÇÃO ORIENTADA A OBJETOS
A programação orientada a objetos (Object-oriented (Object-oriented programming — OOP) é uma técnica de programação atual popular e poderosa. As principais características da OOP são encapsulamento, herança e polimorfismo. O encapsulamento é uma forma de ocultação de informação, ou abstração. A herança tem a ver com a escrita de código reutilizável. O polimorfismo se refere à forma pela qual um único nome pode ter múltiplos significados no contexto da herança. Tendo dado essas definições, precisamos admitir que elas possuem pouco significado para os leitores que nunca ouviram falar de OOP. Entretanto, descreveremos todos esses termos em detalhes no decorrer deste livro. C++ favorece a OOP fornecendo classes, um tipo de dado que combina dados e algoritmos. C++ não é o que algumas autoridades chamariam de uma "linguagem pura de OOP". C++ compatibiliza seus recursos OOP com preocupações em relação à eficiência e o que poderíamos chamar de "praticidade". Essa combinação tornou o C++ a linguagem de OOP mais amplamente utilizada, embora nem sempre seu uso siga estritamente a filosofia da OOP. ■
CARACTERÍSTICAS DO C++
C++ possui classes que permitem sua utilização como uma linguagem orientada a objetos. Admite a sobrecarga de funções e operadores. (Todos esses termos serão explicados ao longo do texto; não fique preocupado se não entender alguns deles.) A ligação do C++ com a linguagem C lhe fornece uma aparência mais tradicional do que a das linguagens orientadas a objetos mais recentes, e, no entanto, ele possui mais mecanismos poderosos de abstração do que muitas das linguagens populares atuais. C++ possui modelos que possibilitam a implementação total e direta da abstração do algoritmo. Os modelos de C++ permitem que se escreva código utilizando parâmetros para tipos. Os mais novos padrões de C++ e a maioria dos compiladores de C++ permitem namespaces múltiplos para possibilitar maior reutilização dos nomes de classes e funções. Os recursos de tratamento das exceções são semelhantes aos encontrados em outras linguagens de programação. O gerenciamento da memória em C++ é semelhante ao de C. O programador deve alocar sua própria memória e lidar com sua própria coleção de lixo. A maioria dos compiladores permitirá que você faça em C++ um gerenciamento de memória estilo C, já que o C é, em essência, um subconjunto de C++. Entretanto, o C++ também tem sua própria sintaxe para um gerenciamento de memória estilo C++, e seria aconselhável que você utilizasse o estilo C++ de gerenciamento de memória ao escrever código em C++. Este livro utiliza apenas o gerenciamento de memória estilo C++. ■
TERMINOLOGIA DO C++
Todas as entidades semelhantes a procedimentos são chamadas de funções funções em C++. Tudo o que é chamado de procedimento , método , função ou subprograma em em outras linguagens é chamado de função em em C++. Como veremos na próxima subseção, um programa em C++ é basicamente apenas uma função chamada main. As outras terminologias de C++ são praticamente as mesmas que as de outras linguagens de programação e serão explicadas quando da apresentação de cada conceito. ■
AMOSTRA DE PROGRAMA EM C++
O Painel 1.1 contém um programa simples em C++ e duas possíveis saídas de tela que podem ser geradas quando um usuário executa o programa. Um programa em C++ é, na realidade, uma definição de função para
Introdução ao C++
3
uma função chamada main. Quando o programa é executado, a função chamada main é invocada. O corpo da função main fica entre chaves, { }.Quando o programa é executado, as declarações entre as chaves são executadas. As duas linhas seguintes fazem com que as bibliotecas com entrada e saída de terminal estejam disponíveis disponíveis para o programa. Os detalhes concernentes a essas duas linhas e tópicos relativos são tratados na Seção 1.3 e nos Capítulos 9, 11 e 12. # include
using namespace std;
A linha seguinte diz que main é uma função sem parâmetros que ao terminar sua execução retornará um valor inteiro int: int main ( )
Alguns compiladores permitirão que você omita o int ou substitua-o por void, o que indica uma função que não retorna nenhum valor. Entretanto, a forma acima é a mais aceita universalmente para iniciar a função main em um programa C++. O programa termina quando o seguinte comando é executado: return 0;
Este comando termina a invocação da função main e fornece 0 como o valor da função. De acordo com o padrão ANSI/ISO C++, este comando não é obrigatório, mas muitos compiladores o exigem. O Capítulo 3 discutirá as funções de C++ em todos os detalhes. Painel 1.1
1 2 3 4 5
Amostra de programa em C++ parte (parte 1 de 2)
#inc #inclu lude de m> using namespace std; int main(
)
{ int numberOfLanguages;
6 7
cout cout << "Olá "Olá, , leit leitor or.\ .\n" n" << "Bem "Bem-v -vin indo do ao C++. C++.\n \n"; ";
8 9
cout cout << "Quant "Quantas as lingua linguagen gens s de program programação ação você você já usou? usou? "; cin cin >> num numberO berOfL fLan angu guag ages es; ;
10 11 12 13 14
if (numberOfLanguages
< 1) cout << "Leia o Prefácio. Prefácio. Talvez você prefira\n" prefira\n" << "um livro mais básico do mesmo autor.\n"; autor.\n";
else
cout cout << "Divir "Divirta-s ta-se!\n e!\n"; ";
15 16 }
return 0;
DIÁLOGO PROGRAMA-USUÁRIO I Olá, leitor. Bem-vindo ao C++. Quantas linguagens de programação você já usou? 0 Leia o Prefácio. Talvez você prefira um livro mais básico do mesmo autor.
O usuário digitou 0 no teclado.
4
Fundamentos do C++
Painel 1.1
Amostra de programa em C++ ( parte 2 de 2)
DIÁLOGO PROGRAMA-USUÁRIO 2 Olá, leitor. Bem-vindo ao C++. Quantas linguagens de programação você já usou? Divirta-se!
1
O usuário digitou 1 no teclado.
A declaração de variáveis em C++ é similar à de outras linguagens de programação. A linha seguinte do Painel 1.1 declara a variável numeroDeLinguagens: int numeroDeLinguagens;
O tipo int é um dos tipos de C++ para números inteiros (integers). Se você nunca programou em C++, o uso de cin e cout para a E/S de terminal deve ser novo para você. Este tópico será abordado mais adiante neste capítulo, mas a idéia geral pode ser observada neste programa-amostra. Por exemplo, considere as duas linhas seguintes do Painel 1.1: cout << "Quantas linguagens de programação você já usou? "; cin >> numeroDeLinguagens;
A primeira linha faz com que o texto entre aspas seja exibido na tela. A segunda linha lê um número que o usuário digita no teclado e estabelece o número digitado como o valor da variável numeroDeLinguagens, As linhas cout << "Leia o Prefácio. Talvez você prefira\n" << "um livro mais básico do mesmo autor. \n";
fazem com que duas strings sejam exibidas, em vez de uma. Os detalhes são explicados na Seção 1.3. O símbolo \n é o caractere de nova linha, que instrui o computador a começar uma nova linha de saída. Embora você possa não estar certo sobre os detalhes exatos de como escrever essas declarações, provavelmente será capaz de adivinhar o significado do comando if-else. Os detalhes serão explicados no próximo capítulo. (A propósito, se você ainda não teve experiência com nenhuma linguagem de programação, deveria ler o prefácio para ver se o livro mais básico de que falamos neste programa não lhe seria mais adequado. Você não precisa ter tido qualquer experiência com C++ para ler este livro, mas é necessária uma experiência mínima com programação.)
1.2
Variáveis, Expressões e Declarações de Atribuição Uma vez que uma pessoa tenha compreendido como as variáveis são usadas na programação, entendeu a quin- tessência da programação. E. W. Dijkstra, Notes on Structured Programming
As variáveis, expressões e atribuições em C++ são similares às da maioria das outras linguagens de finalidade geral. ■ IDENTIFICADORES
O nome de uma variável (ou outro item que possa ser definido em um programa) é chamado de identificador . Um identificador em C++ deve começar com uma letra ou um símbolo de sublinhado, e todos os outros caracteres de vem ser letras, dígitos ou o símbolo de sublinhado. Por exemplo, todos os identificadores seguintes são válidos: x x1 x_1_abc ABC123z7 soma TAXA contagem dado2 grandeBonus
Todos os nomes acima são corretos e deveriam ser aceitos pelo compilador, mas os primeiros cinco são escolhas ruins para identificadores, porque não descrevem o uso do identificador. Nenhum dos identificadores seguintes é correto, e todos seriam rejeitados pelo compilador: 12 3X %troco dado-1 meuprimeiro.c PROG.CPP
Variáveis, Expressões e Declarações de Atribuição
5
Os três primeiros não são permitidos porque não começam com uma letra nem com um caractere de sublinhado. Os três restantes não são identificadores porque contêm símbolos que não são letras, dígitos ou o caractere de sublinhado. Embora seja legal começar um identificador com um sublinhado, você deveria evitar fazer isso, porque os identificadores que começam com um sublinhado são reservados informalmente para identificadores do sistema e bibliotecas-padrão. C++ é uma linguagem que percebe a diferença entre maiúsculas e minúsculas nos identificadores. Assim, os três identificadores a seguir são diferentes e poderiam ser usados para nomear três variáveis diferentes. taxa TAXA Taxa
Entretanto, não é uma boa idéia usar duas variantes desse tipo no mesmo programa, já que isso poderia criar confusão. Embora não seja exigido pelo C++, as variáveis normalmente são escritas com a primeira letra em minúscula. Os identificadores predefinidos, como main, cin, cout e outros, devem ser escritos apenas com letras minúsculas. A convenção que agora está se tornando universal na programação orientada a objetos é escrever nomes de variáveis com uma mistura de letras maiúsculas e minúsculas (e dígitos), começando sempre o nome da variável com letra minúscula e indicando os limites das "palavras" com uma letra maiúscula, como ilustrado nos seguintes nomes de variáveis: velMax, taxaBanco1, taxaBanco2, horaDeChegada
Essa convenção não é tão comum em C++ quanto em algumas outras linguagens orientadas a objetos, mas está sendo usada mais amplamente e é uma boa convenção a seguir. Um identificador C++ pode ter qualquer comprimento, embora alguns compiladores ignorem todos os caracteres excedentes, caso a quantidade de caracteres seja maior que um número especificado. IDENTIFICADORES
Um identificador C++ deve começar com uma letra ou com um caractere de sublinhado, e os caracteres restantes devem ser apenas letras, dígitos, ou o caractere de sublinhado. Os identificadores C++ fazem diferença entre maiúsculas e minúsculas e não têm limite de comprimento.
Há uma classe especial de identificadores, chamados de palavras-chave ou palavras reservadas, que possuem um significado predefinido em C++ e não podem ser usados como nomes de variáveis ou qualquer outra coisa. Neste livro, as palavras-chave aparecem destacadas no texto. Uma lista completa das palavras-chave é fornecida no Apêndice 1. Algumas palavras predefinidas, como cin e cout, não são palavras-chave. Essas palavras predefinidas não fazem parte do núcleo da linguagem C++, e você não é autorizado a redefini-las. Embora essas palavras predefinidas não sejam palavras-chave, são definidas em bibliotecas exigidas pela linguagem C++ padrão. Não é preciso dizer que usar um identificador predefinido para algo diferente do padrão pode criar confusão e perigo; assim, isso deve ser evitado. A prática mais segura e fácil é tratar todos os identificadores predefinidos como se fossem palavras-chave.
■
VARIÁVEIS
Cada variável em um programa em C++ deve ser declarada antes de ser usada. Quando você declara uma variável, está dizendo ao compilador — e, em última análise, ao computador — que tipo de dados serão armazenados na variável. Por exemplo, aqui estão duas definições que podem ocorrer em um programa C++: int numeroDeFeijoes; double umPeso,
totalPeso;
A primeira define a variável numeroDeFeijoes de forma a conter um valor do tipo int, ou seja, um número inteiro. O nome int é uma abreviação de "integer" (inteiro). O tipo int é um dos tipos para números inteiros. A segunda definição declara umPeso e totalPeso como variáveis de tipo double, que é um dos tipos para números com um ponto decimal (conhecidos como números de ponto flutuante ). Como ilustrado aqui, quando há mais de uma variável em uma definição, as variáveis são separadas por vírgulas. Observe também que cada definição termina com um ponto-e-vírgula.
6
Fundamentos do C++
Cada variável deve ser declarada antes de ser usada; obedecida essa regra, podem-se declarar variáveis em qualquer lugar. É óbvio que elas devem sempre ser declaradas em um local que torne o programa mais fácil de ser lido. Normalmente, as variáveis são declaradas logo antes de serem usadas ou no início de um bloco (indicado por uma chave de abertura, { ). Qualquer identificador legal, exceto as palavras reservadas, pode ser usado para um nome de variável. * O C++ possui tipos básicos para caracteres, números inteiros e números de ponto de flutuação (números com um ponto decimal). O Painel 1.2 lista os tipos básicos de C++. O tipo comumente usado para intervalos é int. O tipo char é o tipo para caracteres únicos e pode ser tratado como um tipo inteiro, mas nós não o aconselhamos a fazer isso. O tipo comumente usado para números de ponto flutuante é double e, assim, você deve usar double para números de ponto flutuante, a não ser que tenha alguma razão específica para usar um dos outros tipos de ponto flutuante. O tipo bool (abreviação de booleano ) possui os valores true e false. Não é um tipo inteiro, mas, para se adaptar a códigos antigos, você pode converter bool em qualquer outro dos tipos inteiros e vice-versa. Além disso, a biblioteca-padrão chamada string fornece o tipo string, que é usado para strings de caracteres. O programador pode definir tipos para vetores, classes e apontadores, o que será discutido em capítulos posteriores deste livro. Painel 1.2
Tipos simples
NOME DO TIPO
MEMÓRIA UTILIZADA
INTERVALO
PRECISÃO
short (também chamado short int) int
2 bytes
–32.767 a 32.767
Não aplicável
4 bytes
Não aplicável
long (também chamado long int) float
4 bytes
–2.147.483.647 a 2.147.483.647 –2.147.483.647 a 2.147.483.647
Não aplicável
4 bytes
aproximadamente 7 dígitos 10–38 a 1038 8 bytes aproximadamente 15 dígitos double 10–308 a 10308 10 bytes aproximadamente 19 dígitos long double 10–4932 a 104932 1 byte Todos os caracteres ASCII (Também Não aplicável char pode ser usado como um tipo integer, embora não o recomendemos.) 1 byte Não aplicável bool true, false Os valores listados aqui são apenas valores experimentais para lhe dar uma idéia geral de como os tipos diferem. Os valores para cada um desses registros podem ser diferentes em seu sistema. Precisão refere-se ao número de dígitos significativos, incluindo dígitos na frente do ponto decimal. Os intervalos para os tipos float, double e long double são os intervalos para os números positivos. Os números negativos possuem um alcance similar, mas com um sinal negativo diante de cada número.
DECLARAÇÕES DE VARIÁVEIS Todas as variáveis devem ser declaradas antes de serem usadas. A sintaxe para declarações de variável é a seguinte:
SINTAXE Tipo_Nome Variavel_Nome_1, Variavel_Nome_2, . . . ;
EXEMPLO int count, numeroDeDragoes, numeroDeTrolls; double distancia;
*
O C++ faz uma distinção entre declarar e definir um identificador. Quando um identificador é declarado, o nome é introduzido. Quando é definido, aloca-se espaço para o item nomeado. Para o tipo de variáveis que discutimos neste capítulo e para várias outras partes do livro, o que estamos chamando de declaração de variável declara a variável e a define ao mesmo tempo, ou seja, aloca espaço para a variável. Muitos autores fazem distinção entre definição de variável e declaração de variável. A diferença entre declarar e definir um identificador é mais importante para outros tipos de identificadores, o que encontraremos em outros capítulos. (N. do R.T.)
Variáveis, Expressões e Declarações de Atribuição
7
Cada um dos tipos inteiros possui uma versão sem sinal que inclui apenas valores não-negativos. Esses tipos são unsigned short, unsigned int e unsigned long. Seus intervalos não correspondem exatamente aos intervalos dos valores positivos dos tipos short, int e long, mas tendem a ser maiores (já que usam o mesmo espaço de armazenamento que seus tipos correspondentes short, int ou long, mas não precisam se lembrar do sinal). Dificilmente você precisará desses tipos, mas pode encontrá-los em especificações para funções predefinidas em algumas das bibliotecas de C++, como discutiremos no Capítulo 3. ■
DECLARAÇÕES DE ATRIBUIÇÃO
A forma mais direta de se mudar o valor de uma variável é usar uma declaração de atribuição. Em C++, o sinal de igual é utilizado como um operador de atribuição. Uma declaração de atribuição sempre consiste em uma variável no lado esquerdo do sinal de igual e uma expressão no lado direito. Uma declaração de atribuição termina com um ponto-e-vírgula. A expressão no lado direito de um sinal de igual pode ser uma variável, números, operadores e invocações a funções. Uma declaração de atribuição instrui o computador a avaliar a (ou seja, a calcular o valor da) expressão do lado direito do sinal de igual e fixar o valor da variável do lado esquerdo como igual ao da expressão. Aqui estão alguns exemplos de declarações de atribuição em C++: totalPeso = umPeso * numeroDeFeijoes; temperatura = 98.6; contagem = contagem + 2;
A primeira declaração de atribuição fixa o valor de totalPeso como igual ao número na variável umPeso multiplicado pelo número em numeroDeFeijoes. (A multiplicação é expressa por meio do asterisco, *, em C++.) A segunda declaração de atribuição fixa o valor de temperatura como 98,6. A terceira declaração de atribuição aumenta o valor da variável contagem em 2. DECLARAÇÕES DE ATRIBUIÇÃO Em uma declaração de atribuição, primeiro a expressão do lado direito do sinal de igual é avaliada e depois a variável do lado esquerdo do sinal de igual é fixada como igual a esse valor.
SINTAXE Variavel = Expressao;
EXEMPLOS distancia = velocidade * tempo; contagem = contagem + 2;
Em C++, as declarações de atribuição podem ser usadas como expressões. Quando usadas como expressão, uma declaração de atribuição fornece o valor atribuído à variável. Por exemplo, considere n = (m = 2);
A subexpressão (m = 2) tanto altera o valor de m para 2 quanto fornece o valor 2. Assim, fixa tanto n quanto m como igual a 2. Como você verá quando discutirmos em detalhe a precedência de operadores, no Capítulo 2, podem-se omitir os parênteses; assim, a declaração de atribuição em questão pode ser escrita como n = m = 2;
Nós o aconselhamos a não utilizar uma declaração de atribuição como uma expressão, mas você deve conhecer esse comportamento, porque isso o ajudará a entender certos tipos de erro de código. Por exemplo, isso explicará por que você não receberá uma mensagem de erro quando escrever, erroneamente n = m = 2;
quando queria escrever n = m + 2;
(Este é um erro comum, já que os caracteres = e + ficam na mesma tecla.)
8
Fundamentos do C++ LVALUES E RVALUES Os autores se referem muitas vezes a lvalue e rvalue em livros sobre C++. Um lvalue é qualquer coisa que possa aparecer do lado esquerdo de um operador de atribuição (=), o que significa qualquer tipo de variável. Um rvalue é qualquer coisa que possa aparecer do lado direito de um operador de atribuição, o que significa qualquer expressão que calcule um valor.
Armadilha
VARIÁVEIS NÃO-INICIALIZADAS Uma variável não possui valor com significado até que um programa lhe atribua um. Por exemplo, se a variável numeroMinimo não recebeu um valor nem como lado esquerdo de uma declaração de atribuição nem de outra forma (como a de receber um valor de entrada através de um comando cin), então a linha seguinte está errada: numeroDesejado = numeroMinimo + 10;
Isto porque numeroMinimo não possui um valor com significado e, assim, toda a expressão do lado direito do sinal de igual não possui valor com significado. Uma variável como numeroMinimo, que não recebeu um valor, é chamada de não-inicializada. Essa situação é, na verdade, pior do que se numeroMinimo não tivesse nenhum valor. Uma variável não-inicializada, como numeroMinimo, simplesmente assumirá um valor qualquer. O valor de uma variável não-inicializada é determinado pelo padrão de zeros e uns deixado em sua porção na memória pelo último programa que utilizou aquela porção. Uma forma de evitar uma variável não-inicializada é inicializar as variáveis ao mesmo tempo em que são declaradas. Isso pode ser feito acrescentando-se um sinal de igual e um valor, desta forma: int
numeroMinimo = 3;
Esta linha tanto declara numeroMinimo como uma variável do tipo int como fixa o valor da variável numeroMinimo como igual a 3. Você pode usar uma expressão mais complicada envolvendo operações como adição ou multiplicação quando inicializa uma variável dentro da declaração assim. Os seguintes exemplos declaram três variáveis e inicializam duas delas: double
velocidade = 0.07, tempo, saldo = 0.00;
O C++ permite uma notação alternativa para inicializar variáveis quando estas são declaradas. Essa notação alternativa é ilustrada a seguir, como uma declaração equivalente à anterior: double velocidade(0.07),
tempo, saldo(0.00);
INICIALIZANDO VARIÁVEIS EM DECLARAÇÕES
Você pode inicializar uma variável (ou seja, atribuir-lhe um valor) no momento em que a declara.
SINTAXE Tipo_Nome Variavel_Nome_1 = Expressao_para_Valor_1, Variavel_Nome_2 = Expressao_para_Valor_2, ... ;
EXEMPLOS int
contagem = 0, limite = 10, fatorBobo = 2; double distancia = 999.99;
SINTAXE
Sintaxe alternativa para inicializar em declarações: Tipo_Nome Variavel_Nome_1 (Expressao_para_Valor_1), Variavel_Nome_2 (Expressao_para_Valor_2), ... ;
EXEMPLOS int
contagem(0), limite(10), fatorBobo(2); distancia (999.99);
double
Dica
Nomes de variáveis e outros nomes em um programa devem pelo menos aludir ao significado ou ao uso da coisa que estão nomeando. É muito mais fácil entender um programa se as variáveis possuem nomes com significado. Compare x = y
*
z;
Com os nomes mais descritivos
Variáveis, Expressões e Declarações de Atribuição
9
Dica distancia = velocidade * tempo;
As duas declarações efetuam a mesma coisa, mas a segunda é muito mais fácil de entender.
■
MAIS DECLARAÇÕES DE ATRIBUIÇÃO
Existe uma notação abreviada que combina o operador de atribuição (=) e o operador aritmético de forma que uma dada variável possa ter seu valor alterado por meio de adição ou subtração a um dado valor, multiplicação ou divisão por um dado valor. A forma geral é VariavelOperador = Expressao
que é equivalente a Variavel = VariavelOperador (Expressao)
A Expressao pode ser outra variável, uma constante ou uma expressão aritmética mais complicada. A lista seguinte fornece exemplos: EXEMPLO
EQUIVALENTE A
contagem += 2;
contagem = contagem + 2;
total –= desconto;
total = total – desconto;
bônus *= 2;
bônus = bônus * 2;
tempo /= fatorPressa;
tempo = tempo / fatorPressa;
troco %= 100;
troco = troco % 100;
quantia *= cnt1 + cnt2;
quantia = quantia * (cnt1 + cnt2);
Exercícios de Autoteste 1. Escreva a declaração para duas variáveis chamadas pés* e polegadas.** Ambas as variáveis são do tipo int e devem ser inicializadas com o valor zero na declaração. Forneça as alternativas de inicialização. 2. Escreva a declaração para duas variáveis chamadas contagem e distancia. contagem é do tipo int e inicializada com o valor zero. distancia é do tipo double e inicializada com o valor 1.5. Forneça as alternativas de inicialização. 3. Escreva um programa que contenha declarações que apresentem como saída os valores de cinco ou seis variáveis que tenham sido definidas, mas não inicializadas. Compile e execute o programa. Qual é a saída? Explique. *
■
**
COMPATIBILIDADE DE ATRIBUIÇÃO
Como regra geral, não se pode armazenar um valor de um tipo em uma variável de outro tipo. Por exemplo, a maioria dos compiladores não aceitará as seguintes linhas: int intVariavel;
intVariavel = 2.99;
O problema é uma má combinação de tipos. A constante 2.99 é do tipo double, e a variável intVariavel é do tipo int. Infelizmente, nem todos os compiladores reagirão da mesma forma à declaração de atribuição acima. Alguns emitirão uma mensagem de erro, outros, apenas uma mensagem de alerta, e alguns não apresentarão nenhuma forma de erro. Mesmo se o compilador permitir que você use a atribuição acima, ele dará a intVariavel o valor int 2, não o valor 3. Como você não pode contar com a aceitação do seu compilador à atribuição acima, não deve atribuir um valor double a uma variável de tipo int. * **
Um pé equivale a 30,5 cm no Sistema Internacional de Unidades. (N. do R.T.) Uma polegada equivale a 2,54 cm no Sistema Internacional de Unidades. (N. do R.T.)
10
Fundamentos do C++
Mesmo se o compilador permitir que você misture tipos em uma declaração de atribuição, na maioria dos casos isso não é aconselhável, pois torna seu programa menos portátil, além de causar confusões. Existem alguns casos especiais em que é permitido atribuir um valor de um tipo a uma variável de outro tipo. É aceitável atribuir um valor de um tipo inteiro, como int, a uma variável de tipo ponto flutuante, como o tipo double. Por exemplo, o seguinte estilo é ao mesmo tempo legal e aceitável: double doubleVariavel;
doubleVariavel = 2;
Isto fixará o valor de uma variável chamada doubleVariavel como igual a 2.0. Embora em geral essa seja uma má idéia, você pode armazenar um valor int, como 65, em uma variável de tipo char, e armazenar uma letra como ’Z’ em uma variável de tipo int. Para muitas finalidades, a linguagem C considera os caracteres como pequenos inteiros e, talvez infelizmente, o C++ herdou isso do C. A razão para permitir isso é que as variáveis de tipo char consomem menos memória do que as variáveis de tipo int. Assim, fazer operações aritméticas com variáveis do tipo char pode economizar um pouco de memória. Entretanto, é mais correto utilizar o tipo int quando se lida com inteiros e o tipo char quando se lida com caracteres. A regra geral é que não se pode colocar um valor de um tipo em uma variável de outro tipo — embora possa parecer que há mais exceções à regra do que casos que obedeçam a ela. Mesmo que o compilador não imponha essa regra com muito rigor, é uma boa prática segui-la. Colocar dados de um tipo em uma variável de outro tipo pode causar problemas porque o valor deve ser alterado para um valor do tipo apropriado e esse valor pode não ser aquele esperado. Valores de tipo bool podem ser atribuídos a variáveis de um tipo inteiro ( short, int, long), e inteiros podem ser atribuídos a variáveis do tipo bool. Entretanto, ao fazer isso você prejudica seu estilo. Para maior coerência e para conseguir ler o código de outras pessoas, é bom você saber: quando atribuído a uma variável de tipo bool, qualquer inteiro diferente de zero será armazenado como o valor true. O zero será armazenado como o valor false. Quando se atribui a um valor bool uma variável inteira, true será armazenado como 1, e false será armazenado como 0. ■ LITERAIS
Literal é um nome para um valor específico. Os literais muitas vezes são chamados de constantes, para se contra-
por às variáveis. Literais ou constantes não mudam de valor; variáveis podem mudar de valor. Constantes inteiras são escritas do modo como se costuma escrever números. Constantes de tipo int (ou qualquer outro tipo inteiro) não devem conter um ponto decimal. Constantes de tipo double podem ser escritas em qualquer das duas formas. A forma simples para constantes double é o jeito normal de escrever frações decimais. Quando escrita desta forma, uma constante double deve conter um ponto decimal. Nenhuma constante numérica (inteira ou de ponto flutuante) em C++ pode conter uma vírgula. Uma notação mais complicada para constantes do tipo double é chamada notação científica ou notação de ponto flutuante e é particularmente útil para escrever números muito extensos e frações reduzidas. Por exemplo, 3.67 x 1017que é o mesmo que 367000000000000000.00
é mais bem expresso em C++ pela constante 3.67e17. O número 5.89 x 10 -6, que é o mesmo que 0.00000589, é mais bem expresso em C++ pela constante 5.89e-6. E é o símbolo para expoente e significa "multiplicado por 10 na potência que se segue". O e pode ser escrito em letra maiúscula ou minúscula. Pense no número após o e como aquele que lhe diz a direção e o número de dígitos para mover o ponto decimal. Por exemplo, para mudar 3.49e4 para um número sem um e, mova o ponto decimal quatro casas para a direita para obter 34900.0, que é outra forma de se escrever o mesmo número. Se o número após o e é negativo, mova o ponto decimal pelo mesmo número indicado de casas para a esquerda, inserindo zeros extras se necessário. Assim, 3.49e-2 é o mesmo que 0.0349. O número antes do e pode conter um ponto decimal, embora isso não seja necessário. Entretanto, o expoente depois do e não deve, de modo algum, conter um ponto decimal.
Variáveis, Expressões e Declarações de Atribuição
11
O QUE É DOUBLE? Por que o tipo de números com uma parte fracional é chamado de double? Existe um tipo chamado "single" ("único") que possua a metade do tamanho do double ("duplo")? Não, mas há algo de verdade nisso. Muitas linguagens de programação utilizavam tradicionalmente dois tipos para números com uma parte fracional. Um tipo ocupava menos espaço e era bastante impreciso (ou seja, não permitia muitos dígitos significativos). O segundo tipo ocupava o dobro do espaço na memória e, assim, era bem mais preciso; também permitia números maiores (embora os programadores tendam a se preocupar mais com a precisão do que com o tamanho). Os tipos de números que utilizavam o dobro do espaço eram chamados de números de precisão dupla ; os que utilizavam menos espaço eram chamados de números de precisão simples. Seguindo essa tradição, o tipo que corresponde (mais ou menos) a esse tipo de precisão dupla foi denominado double em C++. O tipo que corresponde à precisão simples em C++ foi denominado float. C++ possui também um terceiro tipo para números com uma parte fracional, que é chamado de long double.
Constantes do tipo char symbol
char são
expressas colocando-se o caractere entre aspas simples, como ilustrado aqui:
= ’Z’;
Observe que a aspa da direita é o mesmo símbolo que a da esquerda. Constantes para strings de caracteres são dadas entre aspas duplas, como ilustrado pela seguinte linha, retirada do Painel 1.1: cout << "Quantas linguagens de programação você já usou? ";
Não se esqueça de que constantes string são colocadas entre aspas duplas, enquanto as de tipo char são colocadas entre aspas simples. Os dois tipos de aspas possuem significados diferentes. Em particular, ’A’ e "A" possuem significados diferentes. ’A’ é um valor de tipo char e pode ser armazenado em uma variável de tipo char. "A" é uma string de caracteres. O fato de que a string contenha apenas um caractere não transforma "A" em um valor de tipo char. Obser ve também que tanto nas strings quanto nos caracteres as aspas do lado esquerdo e direito são as mesmas. Strings entre aspas duplas, como "Oi", muitas vezes são chamadas strings C. No Capítulo 9 veremos que o C++ possui mais de um tipo de string, e esse tipo particular se chama strings C. O tipo bool possui duas constantes, true e false. Essas constantes podem ser atribuídas a uma variável de tipo bool ou usadas em qualquer outro lugar em que uma expressão de tipo bool é permitida. Devem ser escritas só com letras minúsculas. ■
SEQÜÊNCIAS DE ESCAPE
Uma contrabarra, \, precedendo um caractere diz ao compilador que a seqüência que se segue à contrabarra não tem o mesmo significado que o caractere sozinho. Tal seqüência é chamada de seqüência de escape. A seqüência é digitada como dois caracteres sem nenhum espaço entre os símbolos. Existem muitas seqüências de escape definidas em C++. Se você quiser colocar uma contrabarra, \, ou aspas, ", em uma constante string, precisa escapar à capacidade das " de terminar uma constante string utilizando \", ou a capacidade da \ de escapar utilizando \\. \\ diz ao computador que você quer uma barra "ao contrário" de verdade, \, e não uma seqüência de escape; o \" diz que você quer realmente aspas, não o final de uma constante string. Uma \ desgarrada, digamos \z, em uma constante string terá efeitos diferentes em compiladores diferentes. Um compilador pode simplesmente devolver um z; outro pode produzir um erro. O padrão ANSI/ISO afirma que seqüências de escape não-especificadas possuem comportamento indefinido. Isso significa que um compilador pode fazer qualquer coisa que seu autor achar conveniente. A conseqüência é que o código que usa seqüências de escape não-definidas não é portátil. Você não deve utilizar seqüências de escape além daquelas fornecidas pelo padrão C++. Esses caracteres de controle estão listados no Painel 1.3. ■
DANDO NOMES A CONSTANTES
Números em um computador criam dois problemas. O primeiro é que eles não carregam nenhum valor mnemônico. Por exemplo, quando o número 10 é encontrado em um programa, ele não fornece nenhuma pista do seu significado. Se o programa é um programa bancário, pode ser o número de filiais ou o número de caixas na central. Para entender o programa, você precisa conhecer o significado de cada constante. O segundo problema é que, quando é preciso alterar alguns números em um programa, a alteração tende a produzir erros. Suponha que 10
12
Fundamentos do C++
ocorra doze vezes em um programa bancário — quatro vezes ele representa o número de filiais e oito vezes ele representa o número de caixas na central. Quando o banco abrir uma nova filial e o programa precisar ser atualizado, há uma boa possibilidade de que alguns dos 10 que deveriam ser alterados para 11 não sejam, ou que alguns que não deveriam ser alterados sejam. A maneira de evitar esses problemas é dar nome a cada número e utilizar o nome em vez do número dentro do seu programa. Por exemplo, um programa bancário poderia ter duas constantes com os nomes CONTAGEM_DE_FILIAIS e CONTAGEM_DE_CAIXAS. Ambos os números poderiam ter o valor 10, mas, quando o banco abrir uma nova filial, tudo o que você precisa fazer para atualizar o programa é mudar a definição de CONTAGEM_DE_FILIAIS. Painel 1.3
Algumas seqüências de escape
SEQÜÊNCIA
SIGNIFICADO
\n
Nova linha
\r
Sinal de retorno (Posiciona o cursor no início da linha atual. Provavelmente você não o utilizará muito.)
\t
(Horizontal) Tabulação (A vança o cursor até a próxima tabulação.)
\a
Alerta (Soa o sinal de alerta, em geral uma campainha.)
\\
Contrabarra (Permite que você coloque uma contrabarra em uma expressão citada.)
\’
Aspas simples (Em geral usada para colocar aspas simples dentro de uma citação de um caractere.)
\”
spas duplas (Em geral usada para colocar aspas duplas dentro de uma citação em string.)
As seqüências seguintes não costumam ser usadas, mas nós as incluímos para fornecer um quadro completo. \v
Tabulação vertical
\b
Retrocesso
\f
Suprimento de folha (comando que faz a impressora retirar a folha atual)
\?
Interrogação
Como se dá nome a um número em um programa C++? Uma das formas é inicializar uma variável com o valor daquele número, como no seguinte exemplo: int CONTAGEM_DE_FILIAIS int CONTAGEM_DE_CAIXAS
= 10; = 10;
Existe, porém, um problema com esse método de nomear constantes-número: você poderia, inadvertidamente, trocar o valor de uma dessas variáveis. O C++ fornece uma forma de marcar uma variável inicializada de modo que esta não possa ser alterada. Se o seu programa tentar mudar uma dessas variáveis, um erro será produzido. Para marcar uma declaração de variável de modo que o valor da variável não possa ser alterado, coloque antes da declaração a palavra const (que é uma abreviação de constante ). Por exemplo, const int CONTAGEM_DE_FILIAIS const int CONTAGEM_DE_CAIXAS
= 10; = 10;
Se as variáveis são do mesmo tipo, é possível combinar as linhas acima em uma declaração, como se segue: const int CONTAGEM_DE_FILIAIS
= 10, CONTAGEM_DE_CAIXAS = 10;
Entretanto, a maioria dos programadores pensa que colocar cada definição de nome em uma linha separada torna o programa mais claro. A palavra const geralmente é chamada de modificador , porque modifica (restringe) as variáveis declaradas. Uma variável declarada utilizando o modificador const é, em geral, chamada de constante declarada . Escrever constantes declaradas com todas as letras em maiúscula não é uma exigência da linguagem C++, mas é uma prática comum entre os programadores de C++. Uma vez que um número tenha sido nomeado dessa forma, o nome pode então ser usado em qualquer lugar em que o número seja permitido, e terá exatamente o mesmo significado que o número que nomeia. Para alterar uma constante nomeada, você precisa apenas mudar o valor de inicialização na declaração da variável const. O significado de todas as ocorrências de CONTAGEM_DE_FILIAIS, por exemplo, pode ser mudado de 10 para 11 simplesmente alterando-se o valor de inicialização 10 na declaração de CONTAGEM_DE_FILIAIS. O Painel 1.4 contém um programa simples que ilustra o uso do modificador de declaração const.
Variáveis, Expressões e Declarações de Atribuição
13
OPERADORES ARITMÉTICOS E EXPRESSÕES Como a maioria das outras linguagens, o C++ permite que você forme expressões utilizando variáveis, constantes e os operadores aritméticos: + (adição), – (subtração), * (multiplicação), / (divisão) e % (módulo, resto). Essas expressões podem ser usadas em qualquer lugar em que seja legal utilizar um valor do tipo produzido pela expressão. Todos os operadores aritméticos podem ser usados com números de tipo int, números de tipo double e até mesmo com um número de cada tipo. Entretanto, o tipo do valor produzido e o valor exato do resultado dependem dos tipos de números que estão sendo combinados. Se ambos os operandos (ou seja, ambos os números) são de tipo int, então o resultado de combiná-los com um operador aritmético é do tipo int. Se um ou ambos os operandos for do tipo double, então o resultado é do tipo double. Por exemplo, se as variáveis quantiaBase e juros são do tipo int, o número produzido pela expressão seguinte é do tipo int: ■
quantiaBase + juros
Painel 1.4 1 2 3 4 5 6 7
Constante nomeada
#include using namespace std; int main(
)
{ const double TAXA
= 6.9;
double deposito;
8 9
cout << "Digite o total do seu depósito $"; cin >> deposito;
10 11 12 13
double novoBalanco;
14 15 }
return 0;
novoBalanco = deposito + deposito* (TAXA /100); cout << "Em um ano, esse depósito aumentará para\n" << "$" << novoBalanco << "uma quantia pela qual vale a pena esperar.\n";
DIÁLOGO PROGRAMA-USUÁRIO Digite o total do seu depósito R$ 100 Em um ano, esse depósito aumentará para R$ 106.9, uma quantia pela qual vale a pena esperar.
Entretanto, se uma ou ambas as variáveis são do tipo double, então o resultado é do tipo double. Isso também é verdade se você substituir o operador + por qualquer dos outros operadores, –, *, ou /. De modo mais geral, você pode combinar quaisquer tipos aritméticos em expressões. Se todos os tipos forem tipos inteiros, o resultado será de tipo inteiro. Se pelo menos uma das subexpressões for de tipo ponto flutuante, o resultado será de tipo ponto flutuante. O C++ faz o possível para tornar o tipo de uma expressão int ou double, mas, se o valor produzido pela expressão não for um desses tipos devido ao tamanho do valor, um inteiro ou tipo ponto flutuante diferente adequado será produzido. Você pode especificar a ordem das operações em uma expressão aritmética inserindo parênteses. Se você omitir os parênteses, o computador seguirá as chamadas regras de precedência , que determinam a ordem em que as operações, como a adição e a multiplicação, são executadas. Essas regras de precedência são similares às regras utilizadas na álgebra e em outras classes matemáticas. Por exemplo, x + y * z
é calculada fazendo-se primeiro a multiplicação e depois a adição. Exceto em alguns casos-padrão, como na adição de strings ou na multiplicação simples inserida dentro de uma adição, normalmente é melhor incluir os parênteses, mesmo que a ordem pretendida de operações seja aquela ditada pelas regras de precedência. Os parênteses tor-
14
Fundamentos do C++
nam a expressão mais legível e menos sujeita a erros do programador. Um conjunto completo de regras de precedência em C++ é fornecido no Apêndice 2. NOMEANDO CONSTANTES COM O MODIFICADOR const
Quando se inicializa uma variável dentro de uma declaração, pode-se marcar a variável de modo que o programa não tenha a permissão de alterar o seu valor. Para fazer isso, coloque a palavra const na frente da declaração, como descrito abaixo: SINTAXE const
Tipo_Nome Variavel_Nome = Constante;
EXEMPLOS const
int
const
double
■
MAX = 3; PI = 3.14159;
DIVISÃO DE INTEIROS E DE PONTO FLUTUANTE
Quando usado com um ou ambos os operadores do tipo double, o operador de divisão, /, comporta-se como esperado. Entretanto, quando usado com dois operadores do tipo int, o operador de divisão fornece a parte inteira resultante da divisão. Em outras palavras, a divisão inteira descarta a parte depois do ponto decimal. Assim, 10/3 é 3 (não 3.3333...), 5/2 é 2 (não 2.5) e 11/3 é 3 (não 3.6666...). Observe que o número não é arredondado ; a parte depois do ponto decimal é descartada, não importa quão grande seja. O operador % pode ser usado com operadores de tipo int para recuperar a informação perdida quando se usa / para fazer a divisão com números do tipo int. Usado com valores do tipo int, os dois operadores / e % fornecem os dois números produzidos quando se efetua o algoritmo longo da divisão, que você aprendeu na escola. Por exemplo, 17 dividido por 5 dá 3 com resto 2. O operador / fornece o número de vezes que um número "cabe" dentro de outro. O operador % fornece o resto. Por exemplo, os comandos cout << "17 dividido por 5 é " << (17/5) << "\n"; cout << "com um resto de " << (17%5) << "\n";
fornece a seguinte saída: 17 dividido por 5 é 3 com resto 2
Quando usado com valores negativos do tipo int, o resultado dos operadores / e % pode ser diferente para implementações diferentes de C++. Assim, você deve usar / e % com valores int só quando souber que ambos os valores são não-negativos. Armadilha Quando se usa o operador de divisão / com dois inteiros, o resultado é um inteiro. Isso pode ser um problema caso se espere uma fração. Além disso, o problema pode facilmente passar despercebido, resultando em um programa que parece funcionar, mas que produz uma saída incorreta sem que você nem se dê conta. Por exemplo, suponha que você seja um arquiteto paisagista que cobra cinco mil reais (R$ 5.000) por milha* no projeto de uma estrada, e suponha que você conheça a extensão da estrada na qual trabalhará em pés. O preço que você cobrará pode ser facilmente calculado pela seguinte declaração em C++: precoTotal = 5000 * (pes/5280.0);
Isto funciona porque há 5.280 pés em uma milha. Se a extensão da estrada for 15.000 pés, a fórmula lhe dirá que o preço total é 5000 * (15000/5280.0)
Seu programa em C++ obtém o valor final da seguinte forma: 15000/5280.0 é calculado como 2.84. Então o programa multiplica 5000 por 2.84 para obter o valor 14200.00. Com a ajuda do seu programa em C++, você sabe que deveria cobrar R$14.200 pelo projeto. Agora suponha que a variável pes seja do tipo int, e que você se esqueça de colocar o ponto decimal e o zero, de modo que a declaração de atribuição do seu programa fique assim: precoTotal = 5000 * (pes/5280);
*
Uma milha terrestre equivale a 1,609 km. (N. do R.T.)
Variáveis, Expressões e Declarações de Atribuição
15
Armadilha Ainda parece bom, mas causará um problema sério. Se você utilizar esta segunda forma da declaração de atribuição, estará dividindo dois valores de tipo int e, assim, o resultado da divisão pes /5280 é 15000/5280, que é o valor int 2 (em vez do valor 2.84 que você pensa estar obtendo). O valor atribuído a precoTotal é, então, 5000 * 2 ou 10000.00. Se você esquecer o ponto decimal, cobrará R$ 10.000. Entretanto, como já vimos, o valor correto é R$ 14.200. A falta de um ponto decimal lhe custou R$ 4.200. Note que isso será verdade quer o tipo de precoTotal seja int quer seja double; o estrago é causado antes que o valor seja atribuído a precoTotal.
Exercícios de Autoteste 4. Converta cada uma das seguintes fórmulas matemáticas em uma expressão em C++. 3 x + y x + y 3 x 3 x + y 7 z + 2 5. Qual é a saída das seguintes linhas de programa quando estão inseridas em um programa correto que declara todas as variáveis como de tipo char? a = ’b’; b = ’c’; c = a; cout << a << b << c << ’c’; 6. Qual é a saída das seguintes linhas de programa quando estão inseridas em um programa correto que declara número como de tipo int? numero = (1/3) * 3; cout << "(1/3) * 3 é igual a " << numero;
7. Escreva um programa completo em C++ que leia dois números inteiros em duas variáveis de tipo int e então apresente como saída tanto a parte inteira do número quanto o resto quando o primeiro número é dividido pelo segundo. Isso pode ser feito utilizando-se os operadores / e %. 8. Dado o seguinte fragmento que pretende converter graus Celsius em graus Fahrenheit, responda às seguintes questões: double c = 20; double f; f = (9/5) * c + 32.0;
a. Qual é o valor atribuído a f? b. Explique o que está acontecendo na verdade e o que, provavelmente, o programador desejava. c. Reescreva o código como o programador pretendia.
■
CONVERSORES DE TIPOS (CASTING)
Um conversor de tipo, ou casting , é uma forma de alterar um valor de um tipo para um valor de outro tipo. Um cas- ting é um tipo de função que toma o valor de um tipo e produz um valor de outro tipo que seja, em C++, o mais aproximado de um valor equivalente. O C++ possui de quatro a seis tipos diferentes de conversores, dependendo de como eles são contados. Há uma forma mais antiga de conversor de tipo que pode ser expressa por meio de duas notações e quatro formas novas de conversores de tipos introduzidas com o último padrão. Os novos conversores de tipo foram concebidos como substitutos para a forma antiga; neste livro, usaremos os novos. Entretanto, o C++ conserva os velhos conversores com os novos, e, por isso, descreveremos brevemente os velhos também. Vamos começar com os conversores de tipo mais novos. Considere a expressão 9/2. Em C++ essa expressão produz o resultado 4, porque, quando ambos os operandos são de um tipo inteiro, o C++ efetua divisão inteira. Em algumas situações, você pode querer que a resposta seja o valor double 4.5. Você pode obter um resultado de 4.5 utilizando o valor de ponto flutuante "equivalente" 2.0 em lugar do valor inteiro 2, como em 9/2.0, que produz o resultado 4.5. Mas e se o 9 e o 2 forem variáveis de tipo int chamadas n e m? Então n/m dá 4. Se você quiser uma divisão de ponto flutuante neste caso, precisará de um conversor de tipos de int para double (ou outro tipo de ponto flutuante), como no seguinte exemplo: double ans = n/static_cast(m);
16
Fundamentos do C++
A expressão static_cast(m)
é um conversor de tipos. A expressão static_cast é como uma função que toma um argumento int (na realidade, um argumento de praticamente qualquer tipo) e fornece um valor "equivalente" do tipo double. Assim, se o valor de m é 2, a expressão static_cast(m) fornece o valor double 2.0. Observe que static_cast(n) não altera o valor da variável n. Se n possuía o valor 2 antes de a expressão ser calculada, então n ainda possuirá o valor 2 depois de a expressão ser calculada. (Se você sabe o que é uma função em matemática ou em alguma linguagem de programação, pode pensar em static_cast como uma função que fornece um valor "equivalente" de tipo double.) Você pode usar qualquer nome de tipo em lugar de double para obter um conversor de um tipo para o outro. Dissemos que isso produz um valor "equivalente" do tipo-alvo. A palavra equivalente está entre aspas porque não existe uma noção clara de equivalente aplicável entre dois tipos quaisquer. No caso de um conversor de tipo de inteiro para ponto flutuante, o efeito é acrescentar um ponto decimal e um zero. O conversor de tipo na outra direção, de ponto flutuante a inteiro, simplesmente apaga o ponto decimal e todos os dígitos depois dele. Observe que quando a conversão de tipos é de tipo ponto flutuante para inteiro, o número é truncado, não arredondado. static_cast(2.9) é 2, e não 3. Esse static_cast é o tipo mais comum de conversor de tipos e o único que usaremos por algum tempo. Para você ter uma idéia geral e como fonte de referência, listamos todas as quatro formas de conversores de tipo. Algumas podem não fazer sentido até que você chegue aos tópicos relevantes. Se alguma ou todas as outras três formas não fizerem sentido para você a esta altura, não se preocupe. As quatro formas de conversores de tipos são as seguintes: static_cast(Expressao) const_cast(Expressao) dynamic_cast(Expressao) reinterpret_cast(Expressao)
Já falamos sobre static_cast. É um conversor de finalidade geral que se aplica às situações mais "comuns". O const_cast é usado para se desfazer de constantes. O dynamic_cast é usado para descer de um tipo a um tipo inferior em uma hierarquia de herança. O reinterpret_cast é um conversor que depende da implementação, que não discutiremos neste livro e de que dificilmente você necessitará. (Essas descrições podem não fazer sentido para você até que se estudem os tópicos apropriados, nos quais a discussão será mais aprofundada. Por enquanto, usaremos apenas static_cast.) A forma antiga de conversores de tipo é aproximadamente equivalente ao tipo static_cast, mas utiliza uma notação diferente. Uma das duas notações utiliza um nome de tipo como se fosse um nome de função. Por exemplo, int(9.3) fornece o valor int 9; double(42) fornece o valor 42.0. Na segunda notação, equivalente, para a forma antiga de conversor de tipo, escreveríamos (double)42 em vez de double(42). Qualquer das notações pode ser usada com variáveis e expressões mais complicadas e não só com constantes. Embora o C++ conserve esta forma antiga de conversor de tipo, nós o aconselhamos a utilizar as formas mais novas. (Algum dia, a forma antiga desaparecerá, ainda que não haja, por enquanto, nenhum plano para sua eliminação.) Como observamos antes, você sempre pode atribuir um valor de tipo inteiro para uma variável de tipo ponto flutuante, como em double d = 5;
Nesses casos, o C++ efetua uma conversão automática de tipos, convertendo 5 em 5.0 e colocando 5.0 na variável d. Você não pode armazenar o 5 como o valor de d sem uma conversão de tipos, mas às vezes o C++ faz a conversão de tipos para você. Uma conversão automática como essa geralmente é chamada de coerção de tipo. ■ OPERADORES DE INCREMENTO E DECREMENTO
O ++ no nome da linguagem C++ vem do operador de incremento, ++. O operador de incremento acrescenta 1 ao valor de uma variável. O operador de decremento subtrai 1 do valor de uma variável. Normalmente eles são usados com variáveis de tipo int, mas podem ser usados com qualquer tipo numérico. Se n é uma variável de um tipo numérico, então n++ aumenta o valor de n em 1 e n-- diminui o valor de n em 1. Assim, n++ e n-- (quando seguidos por um ponto-e-vírgula) são comandos executáveis. Por exemplo, as linhas
Variáveis, Expressões e Declarações de Atribuição
17
int n
= 1, m = 7; n++; cout << "O valor de n é alterado para " << n << "\n"; m--; cout << "O valor de m é alterado para " << m << "\n";
produzem a seguinte saída: O valor de n é alterado para 2 O valor de m é alterado para 6
Uma expressão como n++ fornece um valor e também altera o valor da variável n. Assim, n++ pode ser usada em uma expressão aritmética como 2*(n++)
A expressão n++ primeiro fornece o valor da variável n, e depois o valor de n é aumentado em 1. Por exemplo, considere o seguinte código: int n
= 2;
int valorProduzido
= 2 * (n++); cout << valorProduzido << "\n"; cout << n << "\n";
Esse código produzirá a saída: 4 3
Observe a expressão 2*(n++). Quando o C++ calcula esta expressão, utiliza o valor que aquele número possuía antes de ser incrementado, não o valor que possui após ser incrementado. Assim, o valor produzido pela expressão n++ é 2, mesmo que o operador de incremento troque o valor de n para 3. Isso pode parecer estranho, mas às vezes é exatamente isso que você deseja. E, como verá a seguir, se você quiser uma expressão que se comporte de maneira diferente, você pode ter. A expressão n++ calcula o valor da variável n e depois o valor da variável n é incrementado em 1. Se você in verter a ordem e colocar o ++ na frente da variável, a ordem dessas duas ações será invertida. A expressão ++n primeiro incrementa o valor da variável n e depois fornece o valor incrementado de n. Por exemplo, considere o código seguinte: int n
= 2;
int valorProduzido
= 2 * (++n); cout << valorProduzido << "\n"; cout << n << "\n";
Esse código é o mesmo que o trecho anterior, a não ser pelo fato de o ++ estar antes da variável. Assim, esse código produzirá a saída seguinte: 6 3
Observe que os dois operadores de incremento n++ e ++n exercem o mesmo efeito sobre a variável n: ambos aumentam o valor de n em 1. Mas as duas expressões chegam a valores diferentes. Lembre-se, se o ++ estiver antes da variável, o incremento é feito antes de o valor ser fornecido; se o ++ estiver depois da variável, o incremento será feito depois que o valor é fornecido. Tudo o que dissemos sobre o operador de incremento se aplica ao operador de decremento, a não ser pelo fato de a variável ser diminuída em 1 em vez de aumentada. Por exemplo, considere o seguinte código: int n
= 8;
int valorProduzido
= n--; cout << valorProduzido << "\n"; cout << n << "\n";
Esse código produzirá a saída:
18
Fundamentos do C++
8 7
Por outro lado, o código int n
= 8; int valorProduzido = --n; cout << valorProduzido << "\n"; cout << n << "\n"; Produz a saída 7 7 n-- fornece lor de n.
o valor de n e depois decrementa n; por outro lado, --n primeiro decrementa n e depois fornece o va-
Não se podem aplicar os operadores de incremento e decremento a algo que não seja uma variável única. Expressões como (x + y)++, --(x + y), 5++, e assim por diante, são todas ilegais em C++. Os operadores de incremento e decremento podem ser perigosos quando utilizados dentro de expressões mais complicadas, como explicado na Armadilha. Armadilha
ORDEM DE EXECUÇÃO Para a maioria dos operadores, a ordem de execução de subexpressões não é garantida. Em particular, normalmente não se pode assumir que a ordem de execução seja da esquerda para a direita. Por exemplo, considere a seguinte expressão: n + (++n)
Suponha que n tenha o valor 2 antes de a expressão ser executada. Então, se a primeira expressão é executada primeiro, o resultado é 2 + 3. Se a segunda expressão é executada primeiro, o resultado é 3 + 3. Como o C++ não garante a ordem de execução, a expressão pode produzir como resultado 5 ou 6. A moral da história é que você não deve programar de uma forma que dependa da ordem de execução, a não ser para os operadores discutidos no próximo parágrafo. Alguns operadores garantem que sua ordem de execução de subexpressões seja da esquerda para a direita. Para os operadores &&(e), || (ou) e o operador vírgula (que será discutido no Capítulo 2), C++ garante que a ordem de execução seja da esquerda para a direita. Felizmente, esses são os operadores para os quais é mais provável que desejemos uma ordem de execução previsível. Por exemplo, considere (n <= 2) && (++n > 2)
Suponha que n possua o valor 2 antes de a expressão ser executada. Nesse caso, você sabe que a subexpressão (n <= 2) é calculada antes de o valor de n ser incrementado. Assim, você sabe que (n <= 2) será true e que o mesmo ocorrerá com a expressão inteira. Não confunda a ordem das operações (por regras de precedência) com a ordem de execução. Por exemplo, (n + 2) * (++n) + 5
sempre quer dizer ((n + 2) * (++n)) + 5
Entretanto, não é claro se o ++n é calculado antes ou depois de n + 2. Qualquer um deles poderia ser calculado primeiro. Agora você sabe por que dissemos que costuma ser uma má idéia usar operadores de incremento (++) ou decremento (--) como subexpressões de expressões maiores. Se isso estiver muito confuso, apenas siga a regra básica de não escrever código que dependa da ordem de execução de subexpressões.
1.3
Entrada/Saída de Terminal Lixo para dentro quer dizer lixo para fora. Ditado do programador
A entrada simples de terminal é feita com os objetos cin, cout e cerr, todos definidos na biblioteca iostream. Para utilizar essa biblioteca, seu programa deve conter as seguintes linhas junto ao início do arquivo contendo seu código:
Entrada/Saída de Terminal
19
#include using namespace std;
■
SAÍDA UTILIZANDO cout
Os valores das variáveis, assim como de strings de texto, podem ser apresentados como saída na tela por meio de cout. Qualquer combinação de variáveis e strings pode ser apresentada. Por exemplo, considere as seguintes linhas do programa no Painel 1.1: cout << "Olá, leitor.\n" << "Bem-vindo ao C++.\n";
Este comando apresenta como saída duas strings, uma por linha. Utilizando cout, você pode encaminhar para a saída qualquer número de itens, seja uma string, uma variável ou expressões mais complicadas. Simplesmente insira um << antes de cada item a ser encaminhado. Como outro exemplo, considere: cout << numeroDeJogos << " jogos realizados.";
O comando diz ao computador para fornecer dois itens: o valor da variável numeroDeJogos e a string entre aspas " jogos realizados.". Observe que você não precisa de uma cópia separada do objeto cout para cada item de saída. É só listar todos os itens a serem encaminhados para a saída, com os símbolos << antecedendo cada um desses itens. O comando único anterior cout é equivalente aos dois seguintes: cout << numeroDeJogos; cout << " jogos realizados.";
Você pode incluir expressões aritméticas em um comando preço e imposto são as variáveis:
cout,
como mostra o exemplo a seguir, em que
cout << "O custo total é de R$" << (preço + imposto);
Os parênteses ao redor de expressões aritméticas, como preço + imposto, são exigidos por alguns compiladores, então é melhor incluí-los. Os dois símbolos < devem ser digitados sem nenhum espaço entre eles. A notação de seta << muitas vezes é chamada de operador de inserção. Todo comando cout termina com um ponto-e-vírgula. Note os espaços dentro das aspas em nossos exemplos. O computador não insere nenhum espaço extra antes ou depois de itens encaminhados para a saída por um comando cout, e é por essa razão que as strings entre aspas nos exemplos muitas vezes começam ou terminam com um espaço em branco. Os brancos impedem que as diversas strings e números se misturem. Se você só precisar de um espaço e não houver strings entre aspas no local em que deseja inserir o espaço, utilize uma string que contenha apenas um espaço, como no exemplo a seguir: cout << primeiroNumero << " " << segundoNumero;
■
NOVAS LINHAS NA SAÍDA
Como foi observado na subseção sobre seqüências de escape, \n diz ao computador para iniciar uma nova linha de saída. A não ser que você diga ao computador para ir para a próxima linha, ele colocará toda a saída na mesma linha. Dependendo de como sua tela é configurada, isso pode produzir qualquer coisa, desde quebras de linha arbitrárias até linhas que saem para fora da tela. Observe que o \n vem entre aspas. Em C++, ir para a próxima linha é considerado um caractere especial, e o modo como se digita esse caractere especial dentro de uma string entre aspas é \n, sem espaço entre os dois símbolos de \n. Embora esse caractere especial seja digitado como dois símbolos, o C++ considera \n como um único caractere, chamado caractere de nova linha . Se você quiser inserir uma linha em branco na saída, pode colocar o caractere de nova linha \n sozinho: cout << "\n";
20
Fundamentos do C++
Outra forma de inserir uma linha em branco é usar endl, que significa essencialmente o mesmo que " \n". Portanto, você também pode obter uma linha em branco na saída assim: cout << endl;
Embora "\n" e endl queiram dizer a mesma coisa, seu uso é levemente diferente: \n deve sempre vir entre aspas, enquanto endl não deve ser colocado entre aspas. Uma boa regra para decidir se você deve usar \n ou endl é a seguinte: se você pode incluir o \n ao final de uma string mais longa, então use \n, como no seguinte exemplo: cout << "O rendimento do combustível é " << mpg << " milhas por galão*\n";
Por outro lado, se o
\n for
ficar sozinho, como na string curta " \n", então é melhor utilizar
endl:
cout << "Você digitou " << numero << endl; INICIANDO NOVAS LINHAS NA SAÍDA
Para iniciar uma nova linha, você pode incluir \n em uma string entre aspas, como no seguinte exemplo: cout << "Você acaba de ganhar\n" << "um dos seguintes prêmios: \n";
Lembre-se de que \n é digitado como dois símbolos sem nenhum espaço entre si. Uma outra alternativa seria começar uma nova linha com endl. Uma forma equivalente de escrever o comando cout acima seria: cout << "Você acaba de ganhar" << endl << "um dos seguintes prêmios:" << endl;
Dica
TERMINE CADA PROGRAMA COM \n OU
endl
É uma boa idéia encaminhar para a saída uma instrução de nova linha ao final de cada programação. Se o último item a ser encaminhado for uma string, inclua um \n ao final da string; se não, inclua um endl como a última ação a ser encaminhada para a saída. Isso atende a dois objetivos. Alguns compiladores não apresentam a saída da última linha do programa se você não incluir uma instrução de nova linha ao final. Em outros sistemas, o programa pode trabalhar bem sem essa instrução final de nova linha, mas o próximo programa a ser executado terá sua primeira linha mesclada à última linha do programa anterior. Mesmo que nenhum desses problemas ocorra em seu sistema, colocar uma instrução de nova linha ao final tornará o programa mais portátil.
■
FORMATANDO NÚMEROS COM UM PONTO DECIMAL
Quando o computador apresenta como saída um valor do tipo double, o formato pode não ser do jeito que você gostaria. Por exemplo, o seguinte comando simples cout pode produzir diversas saídas: cout << "O preço é R$" << preco << endl;
Se
preco tiver
o valor 78.5, a saída poderá ser
O preço é R$ 78.500000
ou O preço é R$ 78.5
ou então a saída poderá ser na seguinte notação (explicada na subseção intitulada
Literais
):
O preço é R$ 7.850000e01
É extremamente improvável que a saída seja a seguinte, mesmo que seja o formato que faz mais sentido: O preço é R$ 78.50
*
Um galão equivale a 3,7806 litros no Sistema Internacional de Unidades. (N. do R.T.)
Entrada/Saída de Terminal
21
Para garantir que a saída esteja na forma que você deseja, seu programa deve conter algumas instruções que digam ao computador como apresentar os números. Há uma "fórmula mágica" que você pode inserir no programa para fazer com que números que contêm um ponto decimal, como números do tipo double, sejam apresentados na saída na notação cotidiana com o número exato de dígitos após o ponto decimal que você especificar. Se você quer dois dígitos após o ponto decimal, utilize a seguinte fórmula mágica: cout.setf(ios::fixed); cout.setf(ios::showpoint); cout.precision(2);
Se você inserir os três comandos acima em seu programa, quaisquer comandos cout que sigam esses comandos apresentarão valores de qualquer tipo de ponto flutuante em notação comum, com exatamente dois dígitos após o ponto decimal. Por exemplo, suponha que o seguinte comando cout apareça em algum lugar depois desta fórmula mágica e suponha que o valor de preço é 78.5. cout << "O preço é R$" << preco << endl;
A saída será apresentada assim: O preço é R$ 78.50
Você pode utilizar qualquer outro número inteiro não-negativo em lugar de 2 para especificar um número diferente de dígitos após o ponto decimal. Pode até utilizar uma variável de tipo int em lugar do 2. Explicaremos esta fórmula mágica em detalhes no Capítulo 12. Por enquanto, pense nela como uma longa instrução que diz ao computador como você deseja que sejam apresentados os números que contêm um ponto decimal. Se desejar alterar o número de dígitos após o ponto decimal de modo que valores diferentes em seu programa sejam apresentados com diferentes números de dígitos, repita a fórmula mágica com algum outro número no lugar de 2. Entretanto, quando você repete a fórmula mágica, só precisa repetir a última linha da fórmula. Se a fórmula mágica já apareceu uma vez em seu programa, a linha seguinte alterará o número de dígitos após o ponto decimal para cinco para todos os valores subseqüentes de qualquer tipo de ponto flutuante apresentado na saída: cout.precision(5); SAÍDA DE VALORES DE TIPO
double
Se você inserir a seguinte "fórmula mágica" em seu programa, todos os números de tipo double (ou qualquer outro tipo de número de ponto flutuante) serão apresentados na saída em notação comum com dois dígitos após o ponto decimal: cout.setf(ios::fixed); cout.setf(ios::showpoint); cout.precision(2);
Você pode usar qualquer outro número inteiro não-negativo no lugar do 2 para especificar um número diferente de dígitos após o ponto decimal. Pode até usar uma variável de tipo int no lugar do 2.
■ SAIDA COM cerr
O objeto cerr é utilizado da mesma forma que cout. O objeto cerr envia sua saída para o fluxo de saída padrão de erro, que normalmente é a tela do terminal. Isso proporciona a você uma forma de distinguir dois tipos de saída: cout para a saída regular e cerr para a mensagem de saída de erro. Se você não fizer nada de especial para alterar as coisas, tanto cout quanto cerr enviarão suas saídas para a tela do terminal, de modo que não há diferença entre eles. Em alguns sistemas é possível redirecionar a saída do programa para um arquivo. Esta é uma instrução do sistema operacional, não do C++, mas pode ser útil. Em sistemas que permitem o redirecionamento de saída, cout e cerr podem ser redirecionados para arquivos diferentes.
22
Fundamentos do C++
■ ENTRADA UTILIZANDO cin
Usa-se cin para a entrada mais ou menos da mesma forma que cout para a saída. A sintaxe é similar, a não ser pelo fato de cin ser usado no lugar de cout e de as setas apontarem para a direção oposta. Por exemplo, no programa no Painel 1.1, a variável numeroDeLinguagens era preenchida pelo seguinte comando cin: cin >> numeroDeLinguagens;
Pode-se listar mais de uma variável em um único comando cin, como ilustrado a seguir: cout << "Digite o número de dragões\n" << "seguido do número de trolls.\n"; cin >> dragoes >> trolls;
Se você preferir, o comando cin acima pode ser escrito em duas linhas: cin >> dragoes >> trolls;
Observe que, como no comando cout, há apenas um ponto-e-vírgula para cada ocorrência de cin. Quando um programa chega a um comando cin, espera que a entrada seja fornecida a partir do teclado e atribui à primeira variável o primeiro valor digitado ao teclado, à segunda variável o segundo valor digitado, e assim por diante. Entretanto, o programa não lê a entrada até que o usuário pressione a tecla Return. Isso permite que o usuário possa utilizar a tecla de retrocesso e corrigir erros na digitação de uma linha de entrada. Os números na entrada devem ser separados por um ou mais espaços ou por uma quebra de linha. Quando se usam comandos cin, o computador ignorará qualquer número de brancos ou quebras de linha até encontrar o próximo valor de entrada. Assim, não importa se os números de entrada são separados por um ou vários espaços, ou mesmo uma quebra de linha. Você pode ler números inteiros, de ponto flutuante ou caracteres por meio de cin. Mais adiante neste livro discutiremos a leitura de outros tipos de dados utilizando cin.
COMANDOS
cin
Um comando cin atribui a variáveis valores digitados no teclado. SINTAXE
cin >> Variavel_1 >> Variavel_2 >>...; EXEMPLOS
cin >> numero >> tamanho; cin >> tempoRestante >> pontosNecessarios;
Dica
QUEBRAS DE LINHA EM E/S
É possível manter a saída e a entrada na mesma linha, e às vezes isso pode produzir uma interface mais agradável para o usuário. Se você simplesmente omitir um \n ou endl ao final da última linha digitada ao prompt, então a entrada do usuário aparecerá na mesma linha que o prompt. Por exemplo, suponha que você use os seguintes comandos de prompt e entrada: cout << "Digite o custo por pessoa: R$"; cin >> custoPorPessoa;
Quando o comando cout é executado, o que aparecerá na tela é: Digite o custo por pessoa: R$
Quando o usuário digitar a entrada, esta aparecerá na mesma linha, assim: Digite o custo por pessoa: R$ 1.25
Estilo de Programa
23
Exercícios de Autoteste 9. Forneça uma declaração de saída que produza a seguinte mensagem na tela: A resposta à questão da Vida, Universo e Sabe Lá o Que Mais é 42.
10. Forneça uma declaração de entrada que preencherá a variável oNumero (de tipo int) com um número digitado ao teclado. Antes da declaração de entrada, coloque uma declaração de prompt pedindo ao usuário para digitar um número inteiro. 11. Que declarações você incluiria em seu programa para assegurar que, quando um número de tipo double for apresentado na saída, ele será apresentado em notação comum com três dígitos após o ponto decimal? 12. Escreva um programa completo em C++ que escreva a frase Olá mundo na tela. O programa não faz nada além disso. 13. Forneça uma declaração de saída que produza a letra ’A’, seguida pelo caractere de nova linha, seguido pela letra ’B’, seguido pelo caractere de tabulação, seguido pela letra ’C’.
1.4
Estilo de Programa Em questões de muita importância, o estilo, não a sinceridade, é que é vital. Oscar Wilde, A Importância de Ser Prudente
O estilo de programação em C++ é similar ao que é usado em outras linguagens. O objetivo é tornar o código fácil de ler e de modificar. Vamos falar um pouco sobre o alinhamento no próximo capítulo. Já falamos a respeito das constantes definidas. Em um programa, a maioria dos literais, senão todos, devem ser constantes definidas. A escolha dos nomes de variáveis e um alinhamento cuidadoso eliminam a necessidade de muitos comentários, mas quaisquer pontos que permanecerem obscuros merecem comentário. ■
COMENTÁRIOS
Existem duas formas de se inserir comentários em um programa em C++. Em C++, duas barras, //, são usadas para indicar o início de um comentário. Todo o texto entre as // e o fim da linha é um comentário. O compilador simplesmente ignora qualquer coisa que se siga às // em uma linha. Se você quiser um comentário que abran ja mais do que uma linha, coloque // em cada linha do comentário. Os símbolos // não possuem um espaço entre si. Outro modo de se inserir comentários em um programa em C++ é usar o par de símbolos /* e */. O texto entre esses símbolos é considerado um comentário e ignorado pelo compilador. Diferentemente dos comentários com //, que requerem // adicionais em cada linha, os comentários entre /* e */ podem abranger várias linhas, como: /* Este é um comentário que abrange três linhas. Observe que não há nenhum símbolo de comentário de nenhum tipo na segunda linha.*/
Comentários do tipo /* */ podem ser inseridos em qualquer lugar em um programa em que um espaço ou quebra de linha seja permitido. Entretanto, eles não devem ser inseridos em qualquer lugar, a não ser que sejam fáceis de ler e não perturbem a estrutura do programa. Normalmente, os comentários são colocados nos finais das linhas ou em linhas separadas. As opiniões diferem em relação a que tipo de comentário é melhor. As duas variedades (o tipo // ou /* */) podem ser eficazes se usadas com cuidado. Um bom método é utilizar os comentários com // em códigos finais e reservar o estilo /* */ para "comentar" o código (fazendo o compilador ignorar o trecho) quando se faz a depuração ( debugging ). É difícil dizer quantos comentários deve conter um programa. A única resposta correta é "somente o necessário", o que, é claro, não significa muito para o programador iniciante. É necessário um pouco de experiência para se saber quando é melhor incluir um comentário. Quando algo é importante e não óbvio, merece um comentário. Entretanto, comentários demais são tão prejudiciais quanto de menos. Um programa que tenha comentários em cada linha estará tão carregado de comentários que a estrutura do programa ficará oculta em um mar de observações óbvias. Comentários como o seguinte não contribuem em nada para a compreensão e não deveriam aparecer em um programa: distancia = velocidade * tempo; // Calcula a distância percorrida.
24
Fundamentos do C++
1.5
Bibliotecas e Namespaces
O C++ vem com diversas bibliotecas-padrão. Essas bibliotecas colocam suas definições em um namespace , que é simplesmente um nome dado a uma coleção de definições. As técnicas para incluir bibliotecas e lidar com namespaces serão discutidas em detalhe mais adiante neste livro. Esta seção tratará apenas dos detalhes necessários para que você utilize as bibliotecas-padrão C++. ■
BIBLIOTECAS E INSTRUÇÕES DE include
O C++ contém diversas bibliotecas-padrão. Na verdade, é quase impossível escrever um programa em C++ sem utilizar pelo menos uma dessas bibliotecas. O jeito normal de se tornar uma biblioteca disponível em um programa é com uma instrução de include. Uma instrução de include para uma biblioteca-padrão possui a forma: #include
Por exemplo, a biblioteca para E/S de terminal é tração começará com
iostream.
Assim, a maioria de nossos programas de demons-
#include
Os compiladores (pré-processadores) podem ser muito rigorosos com as questões de espaço em instruções de include. Assim, é mais seguro digitar uma instrução de include sem nenhum espaço extra: nenhum espaço antes de #, nenhum espaço depois de # e nenhum espaço dentro do <>. Uma instrução de include é simplesmente uma instrução para incluir o texto que se encontra em um arquivo no local especificado pela instrução de include. Um nome de biblioteca é apenas o nome de um arquivo que inclui todas as definições de itens na biblioteca. Mais tarde trataremos das instruções de include para outras coisas que não as bibliotecas-padrão, mas por enquanto só precisaremos de instruções de include para bibliotecas-padrão de C++. Uma lista de algumas bibliotecas-padrão de C++ é fornecida no Apêndice 4. O C++ tem um pré-processador que lida com algumas manipulações textuais simples antes que o texto do seu programa seja dado ao compilador. Algumas pessoas lhe dirão que as instruções de include não são processadas pelo compilador, e sim por um pré-processador . Isso está correto, mas esta é uma diferença com a qual você não deve se preocupar muito. Em quase todos os compiladores, o pré-processador é chamado automaticamente quando você compila o seu programa. Falando tecnicamente, apenas parte da definição da biblioteca é fornecida no arquivo de cabeçalho. Entretanto, neste estágio, esta não é uma distinção importante, já que utilizar a instrução de include com o arquivo de cabeçalho para uma biblioteca (em quase todos os sistemas) fará com que o C++ acrescente automaticamente o que faltar da definição de biblioteca. ■
NAMESPACES
Um namespace é uma coleção de definições de nome. Um nome, como por exemplo um nome de função, pode receber diferentes definições em dois namespaces. Um programa pode, então, utilizar um desses namespaces em um lugar e o outro em outra localização. Vamos tratar dos namespaces em detalhe posteriormente neste livro. Por enquanto, só precisamos falar a respeito do namespace std. Todas as bibliotecas-padrão que usaremos colocam suas definições no namespace std (standard, ou padrão). Para usar qualquer dessas definições em seu programa, você deve inserir as seguintes instruções de using: using namespace std;
Assim, um programa simples que utilize E/S de terminal começará: #include using namespace std;
Se você quiser que alguns nomes em um namespace, mas não todos, fiquem disponíveis em seu programa, há uma forma da instrução de using que torna apenas um nome disponível. Por exemplo, se você quer tornar somente o nome cin do namespace std disponível em seu programa, pode utilizar a seguinte instrução de using:
Bibliotecas e Namespaces
25
using std::cin;
Assim, se os únicos nomes do namespace iniciar seu programa desta forma:
std que
seu programa utilizará forem
cin, count
e
endl,
você poderá
#include using std::cin; using std::cout; using std::endl; em vez de #include using namespace std;
Arquivos de cabeçalho de C++ mais antigos não colocavam suas definições no namespace std; então, se você observar para códigos mais antigos de C++, provavelmente perceberá que os nomes dos arquivos de cabeçalho são escritos de modo ligeiramente diferente e que o código não contém nenhuma instrução de using. Isso é permitido para garantir a compatibilidade com programas mais antigos. Entretanto, você deve usar os arquivos de cabeçalhos das bibliotecas mais novas e a instrução de namespace std.
Armadilha
PROBLEMAS COM NOMES DE BIBLIOTECAS
A linguagem C++ está atualmente em transição. Um novo padrão surgiu, entre outras coisas, com novos nomes para bibliotecas. Se você estiver usando um compilador que ainda não foi revisado para se adaptar aos novos padrões, precisará adotar nomes diferentes de bibliotecas. Se a seguinte instrução não funcionar #include
use #include
De forma similar, outros nomes de bibliotecas são diferentes em compiladores antigos. O Apêndice 5 fornece a correspondência entre os nomes de bibliotecas antigos e novos. Este livro sempre utiliza os nomes dos compiladores mais novos. Se um nome de biblioteca não trabalha com seu compilador, troque-o por um nome de biblioteca correspondente mais antigo. Provavelmente ou todos os novos nomes de bibliotecas funcionarão ou você precisará utilizar todos os nomes de bibliotecas antigos. É improvável que apenas alguns dos nomes de bibliotecas tenham sido atualizados em seu sistema. Se você utiliza os antigos nomes de bibliotecas (aqueles que terminam em .h), não precisa utilizar a instrução using namespace std;
Resumo do Capítulo ■
O C++ faz diferença entre maiúsculas e minúsculas. Por exemplo,
■
Utilize nomes significativos para as variáveis.
count
e
COUNT são
dois identificadores diferentes.
■
As variáveis devem ser declaradas antes de ser usadas. Desde que essa regra seja obedecida, uma declaração de variável pode vir em qualquer lugar.
■
Assegure-se de que as variáveis sejam inicializadas antes que o programa tente utilizar seu valor. Isso pode ser feito quando a variável é declarada com uma declaração de atribuição antes de ser utilizada pela primeira vez.
■
Você pode atribuir um valor de um tipo inteiro, como como double, mas o contrário não é verdadeiro.
int,
a uma variável de tipo de ponto flutuante,
■
Quase todas as constantes numéricas em um programa devem receber nomes com significado que possam ser utilizados em lugar dos números. Isso pode ser feito utilizando-se o modificador const em uma declaração de variável.
■
Use parênteses em expressões aritméticas para tornar clara a ordem das operações.
■
O objeto
cout é
utilizado para a saída de terminal.
26
Fundamentos do C++
■ ■
■ ■
Um \n em uma string entre aspas ou um endl enviado para a saída do terminal inicia uma nova linha de saída. O objeto cerr é utilizado para mensagens de erro. Em um ambiente típico, cerr se comporta da mesma forma que cout. O objeto cin é utilizado para a entrada de terminal. Para usar cin, cout ou cerr, você deve colocar as seguintes instruções junto ao começo do arquivo com o seu programa: #include using namespace std;
■
■
Há duas formas de comentários em C++. Tudo o que se seguir a // na mesma linha é um comentário, e tudo o que estiver entre /* */ é um comentário. Não exagere nos comentários.
RESPOSTAS DOS EXERCÍCIOS DE AUTOTESTE 1.
int pes = 0, polegadas = 0; int pes(0), polegadas(0);
2.
int count = 0; double distancia = 1.5; int count(0); double distancia(1.5);
3. A saída real de um programa como esse depende do sistema e do histórico de uso do sistema. #include using namespace std; int main( )
{ int primeiro, segundo, terceiro, quarto, quinto;
cout<< primeiro << " " << segundo << " " << terceiro << " " << quarto << " " << quinto << "\n"; return 0; }
4. 3*x 3*x + y (x + y)/7 Observe que x + y/7 não é correto. (3*x + y)/(z + 2) 5. bcbc 6. (1/3) * 3 é igual a 0 Como 1 e 3 são do tipo int, o operador / efetua divisão de inteiros, que descarta o resto, assim o valor de 1/3 é 0, não 0.3333… Isso faz com que o valor da expressão toda, 0 * 3, seja igual, é claro, a 0. 7. #include using namespace std; int main( )
{
int numero1, numero2;
cout << "Digite dois números inteiros: "; cin >> numero1 >> numero2; cout << numero1 << " dividido por " << numero2 << " é igual a " << (numero1/numero2) << "\n" << "com resto " << (numero1%numero2)
Projetos de Programação
27
<< "\n";
return 0;
}
8. a. 52.0 b. 9/5 possui valor int 1. Como tanto o numerador quanto o denominador são int, é feita a divisão de inteiros; a parte fracional é descartada. O programador provavelmente queria a divisão de ponto flutuante, que não descarta a parte após o ponto decimal. c. f = (9.0/5) * c + 32.0; ou f = 1.8* c + 32.0; 9. cout << "A resposta para a pergunta da\n" << "Vida, Universo e Sabe Lá o que Mais é 42.\n";
10. cout << "Digite um número inteiro e aperte Return: "; cin >> oNumero;
11. cout.setf(ios::fixed); cout.setf(ios::showpoint); cout.precision(3);
12. #include using namespace std; int main( )
{
cout << "Ola mundo\n"; return 0;
}
13. cout << ’A’ << endl << ’B’ << ’\t’ << ’C’; Outras respostas também são corretas. Por exemplo, as letras poderiam estar entre aspas duplas em vez de aspas simples. Outra resposta possível é a seguinte: cout << "A\nB\tC";
PROJETOS DE PROGRAMAÇÃO 1. Uma tonelada métrica, ou simplesmente tonelada, equivale a 35.273,92 onças. Escreva um programa que leia o peso de um pacote de cereal matinal em onças e apresente na saída o peso em toneladas métricas, assim como o número de caixas necessárias para fornecer uma tonelada métrica de cereal. 2. Um laboratório de pesquisas governamental conclui que um adoçante artificial comumente usado em refrigerantes dietéticos causa a morte de ratos de laboratório. Um amigo seu está desesperado para perder peso e não consegue deixar de tomar refrigerantes. Seu amigo quer saber quanto refrigerante dietético é possível tomar sem morrer. Escreva um programa para dar essa resposta. Os dados de entrada serão a quantidade de adoçante artificial necessária para matar um rato, o peso do rato e o peso da pessoa em dieta. Para garantir a segurança do seu amigo, faça com que o programa requisite o peso com o qual ele deseja ficar, em vez do peso atual. Assuma que o refrigerante dietético contém um décimo de 1% de adoçante artificial. Utilize uma declaração de variável com o modificador const para dar um nome a esta fração. Você pode querer expressar a porcentagem como o valor double 0.001. 3. Os trabalhadores de uma empresa em particular receberam um aumento de 7,6% retroativo a seis meses. Escreva um programa que tome o salário anual anterior de um empregado como entrada e apresente como saída a quantidade de pagamento retroativo devido ao empregado, o novo salário anual e o novo salário mensal. Utilize uma declaração de variável com o modificador const para expressar o aumento de pagamento. 4. Nem sempre é fácil negociar um empréstimo ao consumidor. Uma forma de empréstimo é com abatimento nas prestações, que funciona da seguinte forma: suponha que um empréstimo tenha um valor nominal de R$ 1.000, taxa de juros de 15% e duração de 18 meses. O juro é calculado multiplicando-se o valor nominal de R$ 1.000 por 0,15, o que dá R$ 150. Essa cifra é então multiplicada pelo período do
28
Fundamentos do C++
empréstimo de 1,5 anos, resultando em R$ 225 como o total de juros devidos. Essa quantia é imediatamente deduzida do valor nominal, deixando o consumidor apenas com R$ 775. O reembolso é feito em prestações iguais com base no valor nominal. Assim, o pagamento mensal do empréstimo será R$ 1.000 dividido por 18, que dá R$ 55,56. Escreva um programa que necessitará de três dados de entrada: a quantia que o consumidor precisa receber, a taxa de juros e a duração do empréstimo em meses. O programa deve, então, calcular o valor nominal requerido para que o consumidor receba a quantidade necessária. Deve também calcular o pagamento mensal. 5. Escreva um programa que determine se uma sala de conferências está violando as normas legais de incêndio relativas à sua capacidade máxima. O programa lerá a máxima capacidade da sala e o número de pessoas que comparecerão à conferência. Se o número de pessoas for menor ou igual à capacidade máxima da sala, o programa anuncia que a conferência está de acordo com as normas legais e diz quantas outras pessoas poderão participar conforme essas normas. Se o número de pessoas exceder a capacidade máxima da sala, o programa anuncia que a conferência não poderá ocorrer, devido às normas de incêndio, e diz quantas pessoas devem ser excluídas a fim de obedecer às normas. 6. Um empregado recebe R$ 16,78 por horas regulares trabalhadas em uma semana. Se esse empregado fizer hora extra, deve receber essa mesma taxa multiplicada por 1,5. Do pagamento bruto do empregado, 6% são retidos pela Previdência Social, 14%, pelo Imposto de Renda Federal, 5%, por impostos estaduais, e R$ 10 por semana, para o Sindicato. Se o empregado tiver três ou mais dependentes, um adicional de R$ 35 é retido para cobrir o custo extra do seguro de saúde. Escreva um programa que leia o número de horas trabalhadas em uma semana e o número de dependentes como entrada e apresente como saída o pagamento bruto do empregado, o valor de cada imposto retido e o salário líquido por semana.
Fluxo de Controle CAPÍTULO 2Fluxo de Controle Fluxo de Controle "Você poderia me dizer, por favor, que caminho devo seguir?" "Isso depende muito de para onde você quer ir", disse o Gato. Lewis Carroll, Alice no País das Maravilhas
INTRODUÇÃO Como a maioria das linguagens de programação, C++ lida com o fluxo de controle por meio de comandos de seleção e loopings. Os comandos de seleção e loopings de C++ são semelhantes aos de outras linguagens. São os mesmos da linguagem C e bastante similares aos da linguagem de programação Java. O tratamento de exceções também é um modo de se lidar com fluxo de controle. O tratamento de exceções será abordado no Capítulo 18. 2.1
Expressões Booleanas Aquele que quiser distinguir o verdadeiro do falso precisa ter uma idéia adequada do que é verdadeiro e falso. Benedictus de Spinoza, Ética
A maioria dos comandos de seleção é controlada por expressões booleanas. Uma expressão booleana é qualquer expressão que seja ou verdadeira ou falsa. A forma mais simples de expressão booleana consiste em duas expressões, como números ou variáveis, que são comparadas com um dos operadores de comparação mostrados no Painel 2.1. Obser ve que alguns dos operadores são digitados com dois símbolos, por exemplo, ==, !=, <=, >=. Tenha cuidado para utilizar um sinal duplo de igual, ==, para simbolizar o sinal de igual e para utilizar os dois símbolos != para não-igual. Esses dois operadores-símbolo não devem conter espaços entre os dois símbolos. ■
CONSTRUINDO EXPRESSÕES BOOLEANAS
Você pode combinar duas comparações utilizando o operador "e", que se escreve && em C++. Por exemplo, a seguinte expressão booleana é verdadeira desde que x seja maior que 2 e x seja menor que 7: (2 < x) && (x < 7)
Quando duas comparações são ligadas por meio de um &&, toda a expressão é verdadeira, desde que ambas as comparações sejam verdadeiras; caso contrário, toda a expressão é falsa.
30
Fluxo de Controle
OPERADOR "E", && Você pode formular uma expressão booleana mais elaborada combinando duas expressões booleanas mais simples por meio do o operador "e", &&.
SINTAXE
PARA UMA EXPRESSÃO BOOLEANA (Exp_Booleana_1) && ( Exp_Booleana_2)
EXEMPLO (DENTRO DE UM COMANDO if (
UTILIZANDO &&
if-else)
(resultado > 0) && (resultado < 10) cout << "o resultado está entre 0 e 10.\n";)
else
cout << "o resultado não está entre 0 e 10.\n";
Se o valor de resultado é maior que 0 e também é menor que 10, então o primeiro comando cout será executado; caso contrário, o segundo comando cout será executado. (O comando if-else será abordado em uma próxima seção deste capítulo, mas o significado desse exemplo simples deve ser intuitivamente claro.)
Você pode combinar também duas comparações utilizando o operador "ou", que se escreve || em C++. Por exemplo, a linha seguinte é verdadeira desde que y seja menor que 0 ou y seja maior que 12: (y < 0) || (y > 12)
Quando duas comparações são ligadas por meio de um ||, toda a expressão é verdadeira desde que uma ou ambas as comparações sejam verdadeiras; caso contrário, toda a expressão é falsa. Você pode negar qualquer expressão booleana utilizando o operador !. Se quiser negar uma expressão booleana, coloque a expressão entre parênteses e o operador ! na frente dele. Por exemplo, !(x < y) significa "x não é menor que y". O operador ! pode, muitas vezes, ser evitado. Por exemplo, !(x < y) é equivalente a x >= y. Em alguns casos você pode omitir com segurança os parênteses, mas estes nunca causam nenhum dano. Os detalhes exatos da omissão de parênteses serão dados na subseção intitulada Regras de Precedência . STRINGS DE DESIGUALDADES Não utilize uma string de desigualdades como x < z < y. Se você fizer isso, o programa provavelmente compilará e será executado, mas produzirá uma saída incorreta. Em vez disso, você deve usar duas desigualdades ligadas por um &&, da seguinte forma: (x < z) && (z < y)
Painel 2.1
Operadores de comparação
SÍMBOLO MATEMÁTICO
PORTUGUÊS
NOTAÇÃO C++
C++ SIMPLES
EQUIVALENTE MATEMÁTICO
=
Igual a
==
x + 7 == 2*y
x + 7 = 2y
≠
diferente de
!=
resp != ’n’
resp
<
Menor que
<
contagem < m + 3
≤
Menor ou igual a
<=
tempo <= limite
>
Maior que
>
tempo > limite
≥
Maior ou igual a
>=
idade >= 21
≠
’n’
contagem < m + 3 tempo
≤
limite
tempo > limite idade
≥
21
OPERADOR "OU", || Pode-se formular uma expressão booleana mais elaborada combinando duas expressões booleanas mais simples utilizando o operador "ou", ||.
SINTAXE PARA
UMA EXPRESSÃO BOOLEANA (Exp_Booleana_1) || ( Exp_Booleana_2)
UTILIZANDO ||
Expressões Booleanas
EXEMPLO (DENTRO DE UM COMANDO if (
31
if-else )
(x == 1) || (x == y)) cout << "x é 1 ou x é igual a y.\n";
else
cout << "x não é 1 nem é igual a y.\n"; Se o valor de x é igual a 1 ou o valor de x é igual ao valor de y (ou ambos), então o primeiro comando cout será executado; caso contrário, o segundo comando cout será executado. (O comando if-else será abordado em uma próxima seção deste capítulo, mas o significado desse exemplo simples deve ser intuitivamente claro.)
AVALIANDO EXPRESSÕES BOOLEANAS Como você verá nas próximas duas seções deste capítulo, as expressões booleanas são usadas para controlar os comandos de seleção e loopings. Entretanto, uma expressão booleana possui uma identidade independente do comando de seleção ou looping na qual você possa utilizá-la. Uma variável de tipo bool pode armazenar qualquer um dos valores, true ou false. Assim, você pode fixar uma variável de tipo bool como igual a uma expressão booleana. Por exemplo: ■
bool
resultado = (x < z) && (z < y);
Uma expressão booleana pode ser executada da mesma forma que uma expressão aritmética. A única diferença é que uma expressão aritmética utiliza operações como +, * e / e produz um número como resultado final, enquanto uma expressão booleana utiliza operações relacionais, como == e < e operações booleanas, como &&, ||, ! e produz um dos dois valores true ou false como resultado final. Observe que =, !=, <, <=, etc. atuam em pares de qualquer tipo integrado para produzir um valor booleano true ou false. Primeiro vamos rever o cálculo de uma expressão aritmética. A mesma técnica funcionará para executar expressões booleanas. Considere a seguinte expressão aritmética: (x + 1) * (x + 3)
Presuma que a variável x tenha o valor 2. Para calcular esta expressão aritmética, você calcula as duas somas e obtém os números 3 e 5, e então combina esses dois números utilizando o operador * e obtém 15 como valor final. Observe que, ao efetuar esse cálculo, você não multiplica as expressões (x + 1) e (x + 3). Em vez disso, você multiplica os valores dessas expressões. Você utiliza 3, não (x + 1). Você utiliza 5, não (x + 3). O computador calcula expressões booleanas da mesma forma. As subexpressões são calculadas para obter valores, cada um dos quais é true ou false. Esses valores individuais de true ou false são então combinados de acordo com as regras nas tabelas exibidas no Painel 2.2. Por exemplo, considere a expressão booleana: !((y < 3) || (y > 7))
Painel 2.2
Tabelas da verdade
E
Exp_I
Exp_2
Exp_I
true
true
true
true
false
false
false
true
false
false
false
false
&& Exp_2
OU
Exp_I
Exp_2
Exp_I || Exp_2
true
true
true
true
false
true
false
true
true
false
false
false
NÃO
Exp
! (Exp)
true
false
false
true
32
Fluxo de Controle
que pode ser a expressão controladora para um comando if-else. Suponha que o valor de y é 8. Nesse caso (y < 3) é false e (y > 7) é true. Assim, a expressão booleana apresentada é equivalente a !(false ||
true)
Consultando as tabelas para || (OU), o computador calcula essa expressão dentro dos parênteses como true. Assim, o computador vê toda a expressão como equivalente a !(true)
Consultando as tabelas novamente, o computador vê que !(true) é false, e conclui que false é o valor da expressão booleana original. OS VALORES BOOLEANOS (bool) SÃO
true
E false
true e false são constantes predefinidas de tipo bool. (Devem ser escritas em letras minúsculas.) Em C++, uma expressão booleana produz o valor bool true quando é satisfeita e o valor bool false quando não é satisfeita.
REGRAS DE PRECEDÊNCIA As expressões booleanas (e aritméticas) não precisam ficar todas entre parênteses. Se você omitir os parênteses, a precedência-padrão é a seguinte: ! é executada primeiro, depois as operações relacionais como <, depois && e então ||. Entretanto, é uma boa prática incluir parênteses para tornar a expressão mais fácil de ser entendida. Um lugar em que os parênteses podem seguramente ser omitidos é uma string simples de && ou || (mas não uma mistura dos dois). A seguinte expressão é aceitável em C++, tanto em relação ao compilador quanto à legibilidade: ■
(temperatura > 90) && (umidade > 0.90) && (piscina == ABERTA)
Como os operadores relacionais > e == são executados antes da operação &&, você poderia omitir os parênteses na expressão acima e ela teria o mesmo significado, mas incluir alguns parênteses torna a expressão mais legível. Quando os parênteses são omitidos em uma expressão, o compilador agrupa os itens de acordo com regras conhecidas como regras de precedência . A maioria das regras de precedência em C++ são fornecidas no Painel 2.3. A tabela apresenta um número de operadores que serão discutidos mais adiante neste livro, mas que foram incluídos para deixá-la mais completa e para aqueles que já são capazes de compreendê-los. Painel 2.3
Precedência de operadores ( parte 1 de 2)
::
Operador de resolução de escopo
. [ ] ( ) ++ ––
Operador ponto Seleção de membros Indexação vetorial Chamada de função Operador de incremento em posição de sufixo (colocado após a variável) Operador de decremento em posição de sufixo (colocado após a variável)
++ –– ! – + * & new delete delete [ ] sizeof ( )
Operador de incremento em posição de prefixo (colocado antes da variável) Operador de decremento em posição de prefixo (colocado antes da variável) Não Sinal negativo Sinal positivo Desreferenciação Endereço de Cria (aloca memória) Destrói (libera) Destrói vetor (libera) Tamanho do objeto Casting de tipo
* / %
Multiplicação Divisão Resto (módulo)
→
.
.
Maior precedência (efetuado antes)
Menor precedência (efetuado depois)
Expressões Booleanas Painel 2.3
33
Precedência de operadores ( parte 2 de 2)
Todos os operadores da parte 2 têm menor precedência que os da parte 1.
+ –
Adição Subtração
<< >>
Operador de inserção (saída de console) Operador de extração (entrada de console)
< > <= >=
Menor que Maior que Maior ou igual a Menor ou igual a
== !=
Igual Não-igual
&&
E
||
Ou
= += –= *= /= %=
Atribuição Adição e atribuição Subtração e atribuição Multiplicação e atribuição Divisão e atribuição Módulo e atribuição
?:
Operador condicional
throw
Lança uma exceção
,
Operador vírgula
Menor precedência (efetuado depois)
Se uma operação é executada antes de outra, dizemos que a operação executada primeiro possui maior precedência . Todos os operadores em uma dada caixa no Painel 2.3 possuem a mesma precedência. Os operadores nas caixas mais elevadas apresentam maior precedência que os operadores de caixas mais baixas. Quando os operadores têm a mesma precedência e a ordem não é determinada por parênteses, as operações unárias são realizadas da direita para a esquerda. As operações de atribuição também são feitas da direita para a esquerda. Por exemplo, x = y = z significa x = (y = z). Outras operações binárias que possuem as mesmas precedências são executadas da esquerda para a direita. Por exemplo, x+y+z significa (x+y)+z. Observe que as regras de precedência incluem tanto operadores aritméticos, como + e *, quanto operadores booleanos, como && e ||. Isso porque muitas expressões combinam operações aritméticas e booleanas, como no seguinte exemplo simples: (x + 1) > 2 || (x + 1) < -3
Se você verificar as regras de precedência fornecidas no Painel 2.2, verá que essa expressão equivale a: ((x + 1) > 2) || ((x + 1) < -3)
porque > e < possuem maior precedência que ||. Na realidade, você poderia omitir todos os parênteses na expressão anterior e ela teria o mesmo significado, embora ficasse mais difícil de ler. Ainda que não aconselhemos a omissão de todos os parênteses, pode ser instrutivo ver como tal expressão é interpretada utilizando-se as regras de precedência. Aqui está a expressão sem parênteses: x + 1 > 2 || x + 1 < -3
As regras de precedência dizem para aplicar primeiro o unário -, depois o unário +, então o > e o <, e finalmente o ||, que é exatamente o que a versão cheia de parênteses indicaria. A descrição acima de como uma expressão booleana é executada é basicamente correta, mas em C++ o computador segue um atalho ocasional quando executa uma expressão booleana. Observe que em muitos casos você precisa calcular apenas a primeira de duas subexpressões em uma expressão booleana. Por exemplo, considere a seguinte linha: (x >= 0) && (y > 1)
34
Fluxo de Controle
Se x é negativo, então (x >= 0) é false. Como você pode ver nas tabelas do Painel 2.1, quando uma subexpressão em uma expressão && é false, toda a expressão é false, não importa se a outra expressão é true ou false. Assim, se sabemos que a primeira expressão é false, não há necessidade de avaliar a segunda expressão. Algo similar acontece com expressões com ||. Se a primeira de duas expressões unidas pelo operador || é true, então se sabe que toda a expressão é true, sem importar se a segunda expressão é true ou false. A linguagem C++ utiliza esse fato para, às vezes, poupar a si própria o trabalho de avaliar a segunda subexpressão em uma expressão lógica ligada por um && ou ||. O C++ avalia primeiro a expressão mais à esquerda das duas expressões unidas por && ou ||. Se esta fornece informação suficiente para determinar o valor final da expressão (independentemente do valor da segunda expressão), o C++ não se preocupa em avaliar a segunda expressão. Esse método de avaliação é chamado avaliação curto-circuito. Algumas outras linguagens, que não C++, utilizam a avaliação completa . Na avaliação completa, quando duas expressões estão unidas por && ou ||, ambas as subexpressões são sempre avaliadas e depois se utilizam as tabelas de verdade para obter o valor da expressão final. Tanto a avaliação curto-circuito quanto a avaliação completa fornecem a mesma resposta, então por que você deveria se preocupar se o C++ utiliza a avaliação curto-circuito? Na maioria das vezes, não precisa se preocupar. Desde que ambas as subexpressões unidas por && ou || possuam um valor, os dois métodos produzem o mesmo resultado. Entretanto, se a segunda subexpressão é indefinida, talvez você fique feliz em saber que C++ utiliza a avaliação curto-circuito. Vamos ver um exemplo que ilustra esta questão. Considere as seguintes linhas: if (
(criancas != 0) && (( pedacos/criancas) >= 2) ) cout << "Cada criança pode ficar com dois pedacos!";
Se o valor de criancas não é zero, esse comando não envolve problemas. Entretanto, suponha que o valor de criancas seja zero; considere como a avaliação curto-circuito lida com esse caso. A expressão (criancas != 0) dá como resultado false, então não haveria necessidade de avaliar a segunda expressão. Utilizando a avaliação curto-circuito, C++ diz que toda a expressão é false, sem se preocupar em avaliar a segunda expressão. Isso impede um erro de execução, já que a avaliação da segunda expressão envolveria uma divisão por zero. VALORES INTEIROS PODEM SER USADOS COMO VALORES BOOLEANOS
O C++ às vezes utiliza inteiros como se fossem valores booleanos e valores bool como se fossem inteiros. Em particular, o C++ converte o inteiro 1 em true e o inteiro 0 em false, e vice-versa. A situação é ainda mais complicada do que simplesmente utilizar 1 para true e 0 para false. O compilador tratará qualquer número não-zero como se fosse o valor true e 0 como se fosse o valor false. Desde que você não cometa erros ao escrever expressões booleanas, essa conversão não causa problemas. Entretanto, quando você está fazendo uma depuração (debug ), pode ser útil saber que o compilador aceita combinar inteiros utilizando operadores booleanos &&, || e !. Por exemplo, suponha que você queira uma expressão booleana que seja true desde que o tempo não tenha ainda se esgotado (em algum jogo ou processo). Você poderia utilizar: !tempo > limite
Isso parece perfeito se você ler em voz alta: "não-tempo maior que limite". A expressão booleana está errada, todavia, e o compilador não emitirá uma mensagem de erro. O compilador aplicará as regras de precedência do Painel 2.3 e interpretará sua expressão booleana como o seguinte: (!tempo) > limite
Isso parece absurdo, e intuitivamente é absurdo. Se o valor do tempo é, por exemplo, 36, qual poderia ser o significado de (!tempo)? Afinal, isso é o equivalente a "não 36". Mas em C++ qualquer inteiro não-zero se converte em true e 0 é convertido em false. Assim, !36 é interpretado como "não true" e produz como resultado false, que é convertido novamente em 0, porque estamos fazendo a comparação com um int. O que queremos como valor dessa expressão booleana e o que o C++ nos fornece não é o mesmo. Se tempo possui um valor de 36 e limite, um valor de 60, você quer que a expressão booleana acima produza como resultado true (porque é não true que tempo > limite). Infelizmente, a expressão booleana, em vez disso, é executada da seguinte forma: (!tempo) é false e convertido em 0 e, assim, toda a expressão booleana é equivalente a: 0 > limite
O que, por sua vez, é equivalente a 0 > 60, porque 60 é o valor de limite e isso é avaliado como false. Assim, a expressão lógica acima é avaliada como false, quando você desejaria que fosse true. Há duas formas de corrigir esse problema. Uma forma é usar o operador ! corretamente. Quando usar o operador !, inclua parênteses em torno do argumento. A forma correta de se escrever a expressão booleana acima é
Estruturas de Controle
35
!(tempo > limite)
Outra forma de corrigir o problema é evitar completamente a utilização do operador !. Por exemplo, a linha seguinte também é correta e mais fácil de ler: if (tempo
<= limite)
Quase sempre você pode evitar utilizar o operador !, e alguns programadores aconselham a evitá-lo sempre que possível.
1. Determine o valor, true ou false, de cada uma das seguintes expressões booleanas, presumindo que o valor da variável contagem seja 0 e o valor da variável limite seja 10. Forneça a resposta como um dos valores true ou false. a. (contagem == 0) && (limite < 20) b. contagem == 0 && limite < 20 c. (limite > 20) || (contagem < 5) d. !(contagem == 12) e. (contagem == 1) && (x < y) f. (contagem < 10) || (x < y) g. ! ( ((contagem < 10) || (x < y)) && (contagem >= 0) ) h. ((limite/contagem) > 7) || (limite < 120) i. (limite < 20) || ((limite/contagem) > 7) j. ((limite/contagem) > 7) && (limite < 0) k. (limite < 0) && ((limite/contagem) > 7) l. (5 && 7) + (!6) 2. Às vezes você vê intervalos numéricos fornecidos como 2 < x < 3
Em C++, esse intervalo não possui o significado que você esperaria. Explique e forneça a expressão booleana correta em C++ que especifica que x está entre 2 e 3. 3. Considere a expressão quadrática 2
x - x -
2
Descrever em que intervalo esta quadrática é positiva (ou seja, maior que 0) envolve a descrição de um conjunto de números que são ou menores que a menor raiz (que é -1) ou maiores que a maior raiz (que é 2). Escreva uma expressão booleana em C++ que seja true quando essa fórmula tiver valores positi vos. 4. Considere a expressão quadrática 2
x
- 4x + 3
Descrever onde esta quadrática é negativa envolve a descrição de um conjunto de números que são simultaneamente maiores que a menor raiz (1) e menores que a maior raiz (3). Escreva uma expressão booleana em C++ que seja true quando o valor dessa quadrática for negativo.
2.2
Estruturas de Controle Quando você chegar a uma bifurcação na estrada, siga por ela. Atribuído ao Iogue Berra
■ COMANDOS if-else
Um comando if-else escolhe entre dois comandos alternativos, com base no valor de uma expressão booleana. Por exemplo, suponha que você queira escrever um programa para calcular o salário semanal de um empregado que ganha por hora de trabalho. Presuma que a empresa pague uma hora extra de 1,5 vezes a taxa regular após as primeiras 40 horas trabalhadas. Quando o empregado trabalha 40 horas ou mais, o salário é igual a taxa*40 + 1.5*taxa*(horas - 40)
36
Fluxo de Controle
Entretanto, se o empregado trabalha menos de 40 horas, a fórmula de pagamento correta é simplesmente taxa*horas
O seguinte comando if-else calcula o pagamento correto para um empregado quer este trabalhe menos de 40 horas, quer trabalhe 40 horas ou mais, if (horas
> 40) pagamentoBruto = taxa*40 + 1.5*taxa*(horas - 40);
else
pagamentoBruto = taxa*horas;
A sintaxe para um comando if-else é dada na tabela a seguir. Se a expressão booleana entre parênteses (depois do if) resultar em true, então o comando antes de else é executado. Se a expressão booleana resultar em false, o comando depois de else é executado. COMANDO
if-else
O comando if-else escolhe entre duas ações alternativas, com base no valor de uma expressão booleana. A sintaxe é mostrada abaixo. Observe que a expressão booleana deve estar entre parênteses.
SINTAXE: UM COMANDO ÚNICO PARA CADA ALTERNATIVA if (Expressao_Booleana)
Sim_Comando else
Nao_Comando Se a Expressao_Booleana é true, então Sim_Comando é executada. Se a Expressao_Booleana é false, então Nao_Comando é executada.
SINTAXE: UMA SEQÜÊNCIA
DE
COMANDOS
if (Expressao_Booleana) { Sim_Comando_1 Sim_Comando_2 ... Sim_Comando_Final } else
{
Nao_Comando_1 Nao_Comando_2 ... Nao_Comando_Final }
EXEMPLO if (meusPontos
> seusPontos)
{ cout << "Eu ganhei!\n"; aposta = aposta + 100; } else
{ cout << "Queria que esses pontos fossem de golfe.\n"; aposta = 0; }
Observe que um comando if-else possui comandos menores inseridos em seu interior. A maioria das formas de comandos em C++ permite que se façam comandos maiores a partir de comandos menores combinando os comandos menores de determinado modo. Lembre-se de que, quando você utiliza uma expressão booleana em um comando if-else, a expressão booleana deve estar entre parênteses.
Estruturas de Controle
37
■ COMANDOS COMPOSTOS
Muitas vezes você vai querer que as ramificações de um comando if-else executem mais do que um comando cada uma. Para conseguir isso, encerre os comandos de cada ramificação entre chaves, { e }, como indicado no segundo modelo de sintaxe na caixa intitulada Comando if-else. Uma lista de comandos entre chaves é chamada de comando composto. Um comando composto é tratado como um comando único em C++ e pode ser usado em qualquer lugar em que um comando único possa ser usado. (Assim, o segundo modelo de sintaxe na caixa intitulada Comando if-else é, na verdade, apenas um caso especial do primeiro.) Existem duas formas comumente usadas de alinhar e colocar chaves nos comandos if-else, que são ilustradas abaixo: if (meusPontos
> seusPontos)
{ cout << "Eu ganhei!\n"; aposta = aposta + 100; } else
{ cout << "Queria que esses pontos fossem de golfe.\n"; aposta = 0; }
e if (meusPontos
> seusPontos){ cout << "Eu ganhei!\n"; aposta = aposta + 100; }else{ cout << "Queria que esses pontos fossem de golfe.\n"; aposta = 0; }
A única diferença é na colocação das chaves. Achamos a primeira forma mais fácil de ler e, portanto, é a nossa preferida. A segunda forma economiza linhas e alguns programadores a preferem, da forma como está escrita ou com alguma pequena variação. UTILIZANDO = EM L UGAR DE ==
Infelizmente, é possível escrever muitas coisas em C++ que você pensa ser comandos incorretos, mas que revelam ter algum significado obscuro. Isso significa que, se você cometer um erro e escrever algo que esperaria produzir uma mensagem de erro, pode descobrir que o programa compila e é executado sem mensagens de erro, mas produz um resultado incorreto. Como talvez você não perceba que escreveu algo de modo incorreto, isso pode causar sérios problemas. Por exemplo, considere um comando if-else que comece da seguinte forma: if (x
= 12) Faca_Alguma_Coisa
else
Faca_Outra_Coisa
Suponha que você quisesse testar para ver se o valor de x é igual a 12, de forma que você desejasse realmente utilizar == em vez de =. Você acharia que o compilador perceberia seu erro. A expressão x = 12
não é algo que é satisfeito ou não. É uma declaração de atribuição e, assim, é claro que o compilador deveria emitir uma mensagem de erro. Infelizmente, não é esse o caso. Em C++ a expressão x = 12 é uma expressão que fornece um valor, exatamente como x + 12 ou 2 + 3. O valor de uma expressão de atribuição é o valor transferido para a variável à esquerda. Por exemplo, o valor de x = 12 é 12. Vimos, em nossa discussão sobre a compatibilidade de valores booleanos, que valores int não-zeros são convertidos em true. Se você usar x = 12 como uma expressão booleana em um comando if-else, a expressão booleana sempre será avaliada como true. Esse erro é bem difícil de encontrar, porque parece certo. O compilador pode encontrar o erro sem nenhuma instrução especial se você colocar o 12 no lado esquerdo da comparação: 12 == x não produzirá mensagem de erro, mas 12 = x sim.
38
Fluxo de Controle
5. A seqüência seguinte produz divisão por zero? j = -1; if ((j > 0) && (1/(j+1) > 10)) cout << i << endl; 6. Escreva um comando if-else que apresente como saída a palavra Alto, se o valor da variável pontos for maior que 100, e Baixo, se o valor de pontos for no máximo 100. A variável pontos é do tipo int. 7. Suponha que economias e despesas são variáveis de tipo double que receberam valores. Escreva um comando if-else que apresente como saída a palavra Solvente, subtraia o valor de despesas do valor de economias e atribua o valor 0 a despesas desde que economias seja no mínimo igual a despesas. Se, todavia, economias for menor que despesas, o comando if-else simplesmente apresenta como saída a palavra Falido e não altera o valor de nenhuma variável. 8. Escreva um comando if-else que apresente como saída a palavra Aprovado desde que o valor da variá vel exame seja maior ou igual a 60 e o valor da variável programasFeitos seja maior ou igual a 10. Caso contrário, o comando if-else apresenta como saída a palavra Reprovado. As variáveis exame e programasFeitos são ambas de tipo int. 9. Escreva um comando if-else que apresente como saída a palavra Alerta desde que o valor da variável temperatura seja maior ou igual a 100 ou o valor da variável pressao seja maior ou igual a 200, ou ambos. Caso contrário, o comando if-else apresenta como saída a palavra OK. As variáveis temperatura e pressao são ambas de tipo int. 10. Qual é a saída dos seguintes trechos? Explique suas respostas. a. if(0) cout << "0 é true"; else
cout << "0 é false"; cout << endl; b. if(1) cout << "1 é true"; else
cout << "1 é false"; cout << endl; c. if(-1) cout << "-1 é true"; else
cout << "-1 é false"; cout << endl; Observação: Isto é apenas um exercício, e não foi elaborado para ilustrar estilos de programação que você deveria seguir.
■ OMITINDO O else
Às vezes você deseja que uma das alternativas de um comando if-else não faça absolutamente nada. Em C++, isso pode ser realizado omitindo-se a parte do else. Esses tipos de comando são conhecidos como comandos if , para diferenciá-los dos comandos if-else. Por exemplo, o primeiro dos dois comandos seguintes é um comando if: if (vendas
>= minimo) salario = salario + bonus; cout << "salario = R$" << salario;
Se o valor de vendas for maior ou igual ao valor de minimo, a declaração de atribuição é executada e depois o comando cout seguinte. Por outro lado, se o valor de vendas for menor que minimo, a declaração de atribuição não é executada. Assim, o comando if não provoca nenhuma alteração (ou seja, nenhuma bonificação é acrescentada ao salário-base) e o programa procede diretamente para o comando cout.
Estruturas de Controle ■
39
COMANDOS ANINHADOS
Como já vimos, comandos if-else e if contêm comandos menores dentro deles. Até agora utilizamos comandos compostos e simples, tais como declarações de atribuição, ou subcomandos menores, mas há outras possibilidades. Na realidade, qualquer comando pode ser utilizado como subparte de um comando if-else ou de outros comandos que possuam um ou mais comandos dentro deles. Quando aninhamos comandos, normalmente se alinha cada nível de subcomandos aninhados, embora existam algumas situações especiais (como uma ramificação if-else de seleções múltiplas) em que essa regra não é seguida. ■
COMANDO if-else DE SELEÇÕES MÚLTIPLAS
O comando if-else de seleções múltiplas não é, na realidade, um tipo diferente de comando em C++. É apenas um comando if-else comum aninhado dentro de comandos if-else, mas é considerado um tipo de comando, e é alinhado diferentemente de outros comandos aninhados para refletir essa idéia. A sintaxe para um comando if-else de seleções múltiplas e um exemplo básico são fornecidos na caixa que acompanha esta seção. Observe que as expressões booleanas estão alinhadas uma com a outra e suas ações correspondentes também estão alinhadas umas com as outras. Isso torna fácil observar a correspondência entre expressões booleanas e ações. As expressões booleanas são avaliadas em ordem até que uma expressão booleana true seja encontrada. A essa altura, a avaliação das expressões booleanas pára e a ação correspondente à primeira expressão booleana true é executada. O else final é opcional. Se existir um else final e todas as expressões booleanas forem false, a ação final é executada. Se não existir um else final e todas as expressões booleanas forem false, nenhuma ação é executada. COMANDO SINTAXE
if-else
DE SELEÇÕES MÚLTIPLAS
if (Expressao_Booleana_1)
Comando_1 else if (Expressao_Booleana_2) Comando_2 . . . else if (Expressao_Booleana_n) Comando_n else
Comando_Para_Todas_As_Outras_Possibilidades
EXEMPLO if ((temperatura
< -10) && (dia == DOMINGO)) cout << "Fique em casa."; else if (temperatura < - 10) // e dia != DOMINGO cout << "Fique em casa, mas ligue para o trabalho."; else if (temperatura <= 0) // e temperatura >= -10 cout << "Vista roupas quentes."; else // temperatura > 0 cout << "Vá firme, trabalhe duro."; As expressões booleanas são verificadas em ordem até a primeira expressão booleana true ser encontrada, e então o comando correspondente é executado. Se nenhuma das expressões booleanas é true, o Comando_Para_Todas_As_Outras_Possibilidades é executado.
11. Que saída será produzida pelo seguinte código? int x = 2; cout << "Início\n";
40
Fluxo de Controle
if (x <= 3) if (x != 0)
cout << "Olá do segundo if.\n"; else
cout << "Olá do else.\n"; cout << "Fim\n"; cout << "Início de novo\n"; if (x > 3) if (x != 0) cout << "Olá do segundo if.\n"; else
cout << "Olá do else.\n"; cout << "Fim de novo\n";
12. Que saída será produzida pelo seguinte código? int extra = 2; if (extra < 0)
cout << "pequeno"; else if (extra == 0)
cout << "médio"; else
cout << "grande";
13. Qual seria a saída do Exercício de Autoteste 12 se a atribuição fosse alterada para a seguinte? int extra = -37;
14. Qual seria a saída do Exercício de Autoteste 12 se a atribuição fosse alterada para a seguinte? int extra = 0;
15. Escreva um comando if-else de seleções múltiplas que classifique o valor de n, uma variável int, em uma das seguintes categorias e redija uma mensagem apropriada. n < 0 ou 0
■ COMANDO
≤ n ≤ 100
ou n > 100
switch
O comando switch é o único tipo de comando C++ que implementa ramificações de seleções múltiplas. A sintaxe para um comando switch e um exemplo simples são mostrados na caixa que acompanha esta seção. Quando um comando switch é executado, uma das várias ramificações diferentes é executada. A escolha da ramificação a executar é determinada por uma expressão de controle dada entre parênteses, após a palavra-chave switch. A expressão de controle para um comando switch sempre deve fornecer um valor bool, uma constante enum (de que falaremos mais adiante neste capítulo), um dos tipos inteiros ou um caractere. Quando o comando switch é executado, essa expressão de controle é avaliada e o computador procura entre os valores da constante fornecidos após as várias ocorrências dos identificadores case. Se ele encontra uma constante que seja igual ao valor da expressão de controle, executa o código para esse case. Não se pode ter duas ocorrências de case com o mesmo valor de constante porque isso criaria uma instrução ambígua. COMANDO switch SINTAXE switch (Expressao_De_Controle)
{ case Constante_1:
Sequencia_Do_Comando_1 break; case Constante_2:
Sequencia_Do_Comando_2 break;
. . .
Você não precisa colocar um comando break em cada case. Se omitir um break, esse case continua até encontrar um break (ou até o final do comando switch ).
Estruturas de Controle
41
case Constante_n:
Sequencia_Do_Comando_n break; default:
Sequencia_Do_Comando_Default
} EXEMPLO int classeVeiculo; double pedagio;
cout << "Informe a classe do veículo: "; cin >> classeVeiculo; switch (classeVeiculo)
{ case 1:
cout << "Carro de passageiro."; pedagio = 0.50;
Se você esquecer esse break, os carros de passageiro pagarão R$1,50
break; case 2:
cout << pedagio ; break case 3: cout << pedagio
"Ônibus."; = 1.50;
"Caminhão."; = 2.00;
break; default:
cout << "Classe de veículo desconhecida!"; }
O comando switch termina quando um comando break é encontrado ou quando se chega ao fim do comando switch. Um comando break é formado pela palavra-chave break seguida por um ponto-e-vírgula. Quando o computador executa os comandos depois de um case, continua até chegar a um comando break. Quando o computador encontra um comando break, o comando switch se encerra. Se você omitir o comando break, então, depois de executar o código para um case, o computador continuará e executará o código do próximo case. Observe que se pode ter dois case para a mesma seção de código, como no seguinte trecho de um comando switch: case ’A’: case ’a’:
cout << "Excelente." << "Você não precisa tirar o final.\n"; break;
Como o primeiro case não possui comando break (na realidade não possui comando nenhum), o efeito é o mesmo que se houvesse dois rótulos para um case, mas a sintaxe do C++ exige uma palavra-chave para cada rótulo, como ’A’ e ’a’. Se nenhum rótulo de case possuir uma constante que iguale o valor da expressão de controle, então os comandos que se seguirem ao rótulo default serão executados. Não é preciso haver uma seção default. Se não houver seção default e nenhuma constante igualar o valor da expressão de controle, então nada acontecerá quando o comando switch for executado. Entretanto, é mais seguro ter sempre uma seção default. Se você acha que seus rótulos case listam todas as saídas possíveis, pode colocar uma mensagem de erro na seção default.
42
Fluxo de Controle
ESQUECENDO UM break EM UM COMANDO switch
Se você esquecer um break em um comando switch, o compilador não emitirá uma mensagem de erro. Você terá escrito um comando switch sintaticamente correto, mas que não fará o que você pretendia que fizesse. Observe a anotação no exemplo na caixa intitulada Comando switch.
USE COMANDOS switch PARA MENUS
O comando if-else de seleções múltiplas é mais versátil que o comando switch, e você pode usar um comando if-else de seleções múltiplas em qualquer lugar em que seja possível utilizar um comando switch. Entretanto, às vezes o comando switch é mais claro. Por exemplo, o comando switch é perfeito para implementar menus. Cada ramificação do comando switch pode ser uma opção do menu.
■
TIPO ENUMERAÇÃO
Um tipo enumeração é um tipo cujos valores são definidos por uma lista de constantes de tipo int. Um tipo enumeração é parecido com uma lista de constantes declaradas. Tipos enumeração podem ser úteis para definir uma lista de identificadores para usar como rótulos case em um comando switch. Quando se define um tipo enumeração, podem-se usar quaisquer valores int e pode-se definir qualquer número de constantes. Por exemplo, o seguinte tipo enumeração define uma constante para a duração de cada mês: enum DuracaoDoMes
{ DURACAO_JAN = 31, DURACAO_FEV = 28, DURACAO_MAR = 31, DURACAO_ABR = 30, DURACAO_MAI = 31, DURACAO_JUN = 30, DURACAO_JUL = 31, DURACAO_AGO = 31, DURACAO_SET = 30, DURACAO_OUT = 31, DURACAO_NOV = 30, DURACAO_DEZ = 31 };
Como mostra esse exemplo, duas ou mais constantes nomeadas em um tipo enumeração podem receber o mesmo valor int. Se você não especificar nenhum valor numérico, são atribuídos valores consecutivos aos identificadores, a começar do 0. Por exemplo, a definição do tipo enum Direcao
{ NORTE = 0, SUL = 1, LESTE = 2, OESTE = 3 };
é equivalente a enum
Direcao { NORTE, SUL, LESTE, OESTE };
A forma que não lista explicitamente os valores int normalmente é usada quando você deseja uma lista de nomes e não se importa com que valores estes possuem. Suponha que você inicialize uma constante de enumeração com algum valor, digamos enum MinhaEnum
{ UM = 17, DOIS, TRES, QUATRO = -3, CINCO };
então UM assume o valor 17; DOIS assume o próximo valor int, 18; TRES assume o próximo valor, 19; QUATRO assume -3 e CINCO assume o próximo, -2. Em suma, o padrão para a primeira constante de enumeração é 0. O resto vai aumentando de 1 em 1, a não ser que se defina uma ou mais constantes de enumeração. Embora as constantes em um tipo enumeração sejam dadas como valores int e possam ser usadas como inteiros em muitos contextos, lembre-se de que um tipo enumeração é um tipo separado e tratado como um tipo diferente do tipo int. Utilize tipos enumeração como rótulos e evite fazer operações aritméticas com variáveis de um tipo enumeração. ■
OPERADOR CONDICIONAL
É possível inserir um condicional dentro de uma expressão, utilizando um operador ternário conhecido como operador condicional (também chamado de operador ternário ou if aritmético ). Seu uso decorre de um velho estilo de programação e não o aconselhamos a utilizá-lo. Está incluído aqui em nome da abrangência (caso você discorde de nosso estilo de programação). O operador condicional é uma variante de notação em certas formas do comando if-else. Essa variante está ilustrada a seguir. Considere o comando
Loops
43
if (n1
> n2) max = n1;
else
max = n2;
Isto pode ser expresso utilizando o operador condicional da seguinte forma: max = (n1 > n2) ? n1 : n2;
A expressão do lado direito da declaração de atribuição é a expressão de operador condicional: (n1 > n2) ? n1 : n2
O ? e o : juntos formam um operador ternário conhecido como o operador condicional. Uma expressão de operador condicional começa com uma expressão booleana seguida por um ? e depois seguida por duas expressões separadas por dois-pontos. Se a expressão booleana é true, a primeira das duas expressões é fornecida; caso contrário, a segunda das duas expressões é que é fornecida.
16. Dadas as seguintes declarações e comando de saída, presuma que tenham sido inseridos em um programa correto, que é executado. Qual é a saída? enum Direcao { N, S, L, O }; //... cout << O << " " << L << " " << S << " " << N << endl; 17. Dadas as seguintes declarações e comando de saída, presuma que tenham sido inseridos em um programa correto, que é executado. Qual é a saída? enum Direcao { N = 5, S = 7, L = 1, O }; //... cout << O << " " << L << " " << S << " " << N << endl;
2.3
Loops Não é verdade que a vida seja uma chatice atrás da outra. É uma chatice que fica se repetindo. Edna St. Vincent Millay Carta a Arthur Darison Ficke, 24 de outubro de 1930
Os mecanismos de loop em C++ são semelhantes aos de outras linguagens de alto nível. Os três comandos loop em C++ são o comando while, o comando do-while e o comando for. A mesma terminologia utilizada em outras linguagens é utilizada em C++. O código repetido em um loop é chamado corpo do loop. Cada repetição do corpo do loop é chamada de uma iteração do loop. ■ COMANDOS while E do-while
A sintaxe para o comando while e sua variante, o comando do-while, é fornecida na caixa que acompanha a seção. Em ambos os casos, a sintaxe do corpo de múltiplos comandos é um caso especial da sintaxe para um loop com um corpo de um comando único. O corpo de comandos múltiplos é um comando composto de comandos únicos. Exemplos de um comando while e de um comando do-while são fornecidos nos Painéis 2.4 e 2.5. SINTAXE PARA COMANDOS UM COMANDO
while
while COM UM
E do-while
CORPO
DE
COMANDO ÚNICO
CORPO
DE
COMANDOS MÚLTIPLOS
while (Expressao_Booleana)
Comando
UM COMANDO
while COM UM
while (Expressao_Booleana)
44
Fluxo de Controle
{
Comando_1 Comando_2 . . . Comando_Final }
UM COMANDO do-while
COM UM
CORPO
DE
COMANDO ÚNICO
CORPO
DE
COMANDOS MÚLTIPLOS
do
Comando while (Expressao_Booleana);
UM COMANDO do-while
COM UM
do
{
Comando_1 Comando_2 . . . Comando_Final } while (Expressao_Booleana);
Painel 2.4
1 2 3 4 5
Exemplo de um comando while parte ( 1 de 2)
#include using namespace std; int main( )
{ int countDown;
6 7
cout << "Quantas saudações você quer? "; cin >> countDown;
8 9 10 11 12
while (countDown > 0)
{ cout << "Olá "; countDown = countDown - 1; }
13 14
cout << endl; cout << "Acabou! \n";
15 16 }
return 0;
DIÁLOGO PROGRAMA-USUÁRIO 1 Quantas saudações você quer? 3 Olá Olá Olá Acabou!
Não se esqueça do ponto-e-vírgula final.
Loops Painel 2.4
45
Exemplo de um comando while (parte 2 de 2)
DIÁLOGO PROGRAMA-USUÁRIO 2 Quantas saudações você quer? 0 O corpo do loop é executado zero vezes.
Acabou!
Painel 2.5
1 2 3 4 5
Exemplo de um comando do-while
#include using namespace std; int main( )
{ int countDown;
6 7
cout << " Quantas saudações você quer? "; cin >> countDown;
8 9 10 11 12
do
{ cout << "Olá "; countDown = countDown - 1; }while (countDown > 0);
13 14
cout << endl; cout << "Acabou!\n";
15 16 }
return 0;
DIÁLOGO PROGRAMA-USUÁRIO 1 Quantas saudações você quer? 3 Olá Olá Olá Acabou!
DIÁLOGO PROGRAMA-USUÁRIO 2 Quantas saudações você quer? 0 Olá Acabou!
O corpo do loop é executado pelo menos uma vez.
A diferença importante entre os loops while e do-while envolve o momento em que a expressão booleana de controle é verificada. Com um comando while, a expressão booleana é verificada antes que o corpo do loop seja executado. Se a expressão booleana é avaliada como false, o corpo não é executado. Com um comando do-while, o corpo do loop é executado primeiro, e a expressão booleana é verificada depois que o corpo do loop é executado. Assim, o comando do-while sempre executa o corpo do loop pelo menos uma vez. Depois deste início, o loop while e o do-while se comportam da mesma forma. Após cada iteração do corpo do loop, a expressão booleana é verificada novamente; se for true, o loop é iterado outra vez. Caso tenha mudado de true para false, o comando loop se encerra.
46
Fluxo de Controle
A primeira coisa que acontece quando um loop while é executado é que a expressão booleana de controle é avaliada. Se a expressão booleana é avaliada como false a essa altura, o corpo do loop nunca é executado. Pode parecer inútil executar o corpo de um loop zero vezes, mas às vezes esta é a ação desejada. Por exemplo, muitas vezes se usa um loop while para somar uma lista de números, mas a lista poderia estar vazia. Para sermos mais específicos, um programa de contabilidade de cheques poderia usar um loop while para somar os valores de todos os cheques que você emitiu em um mês — mas você pode ter tirado um mês de férias, em que não emitiu nenhum cheque. Nesse caso, há zeros a serem somados e, assim, o loop é iterado zero vezes. NOVA VISÃO DOS OPERADORES DE INCREMENTO E DECREMENTO Em geral não aconselhamos o uso de operadores de incremento e decremento em expressões. Entretanto, muitos programadores gostam de utilizá-los nas expressões booleanas de controle de um comando while ou do-while. Se realizado com cuidado, isso pode funcionar bem. Apresentamos um exemplo no Painel 2.6. Não se esqueça de que em contagem++ <= numeroDeItens, o valor fornecido por contagem++ é o valor de contagem antes de ser incrementado. ■
Painel 2.6
Operador de incremento em uma expressão
1 2
#include using namespace std;
3 4 5 6
int main( )
{ int numeroDeProdutos, count,
caloriasPorProduto, totalCalorias;
7 8
cout << "Quantos produtos você consumiu hoje? "; cin >> numberOfItems;
9 10 11 12
totalCalories = 0; count = 1; cout << "Informe o número de calorias em cada um dos\n" << numberOfItems << " produtos consumidos:\n";
13 14 15 16 17 18 19 20 21 22 }
while (count++ <= numberOfItems)
{ cin >> caloriesForItem; totalCalories = totalCalories + caloriesForItem; } cout << "Total de calorias consumidas hoje = " << totalCalories << endl; return 0;
DIÁLOGO PROGRAMA-USUÁRIO Quantos produtos você consumiu hoje? 7 Informe o número de calorias em cada um dos 7 produtos consumidos: 300 60 1200 600 150 1 120
Total de calorias consumidas hoje = 2431
Loops
47
18. Qual é a saída do seguinte trecho de programa? int contagem
= 3; while (contagem-- > 0) cout << contagem << " ";
19. Qual é a saída do seguinte trecho de programa? int contagem
= 3;
while (--contagem
> 0) cout << contagem << " ";
20. Qual é a saída do seguinte trecho de programa? int n
= 1;
do
cout << n << " "; <= 3);
while (n++
21. Qual é a saída do seguinte trecho de programa? int n
= 1;
do
cout << n << " "; while (++n <= 3);
22. Qual é a saída do seguinte trecho de programa? (x é de tipo int.) int x
= 10; while (x > 0) { cout << x << endl; x = x - 3; }
23. Que saída seria produzida no exercício anterior se o sinal > fosse substituído por 24. Qual é a saída do seguinte trecho de programa? (x é de tipo int.) int x
= 10;
do
{ cout << x << endl; x = x - 3; } while (x > 0);
25. Qual é a saída do seguinte trecho de programa? (x é de tipo int.) int x
= -42;
do
{ cout << x << endl; x = x - 3; } while (x > 0);
26. Qual é a diferença mais importante entre um comando while e um comando do-while?
■
OPERADOR VÍRGULA O operador vírgula é uma forma de avaliar uma lista de expressões e fornecer o valor da última expressão. Às
vezes é útil empregar um loop for, como indicado em nossa discussão sobre o loop for na próxima subseção. Não o aconselhamos a utilizá-lo em outros contextos, mas seu uso é legal em qualquer expressão. O operador vírgula é ilustrado pela seguinte declaração de atribuição: resultado = (primeiro = 2, segundo = primeiro + 1);
O operador vírgula é a vírgula mostrada no exemplo. A expressão vírgula é a expressão do lado direito do operador de atribuição. O operador vírgula possui duas expressões como operandos. Nesse caso, os dois operandos são primeiro = 2 e segundo = primeiro + 1
48
Fluxo de Controle
A primeira expressão é avaliada, depois a segunda. Como dissemos no Capítulo 1, a declaração de atribuição, quando usada como uma expressão, fornece o novo valor da variável do lado esquerdo do operador de atribuição. Assim, essa expressão vírgula fornece o valor final da variável segundo, o que significa que a variável resultado é fixada como igual a 3. Como apenas o valor da segunda expressão é fornecido, a primeira expressão é avaliada apenas por seus efeitos colaterais. No exemplo anterior, o efeito colateral da primeira expressão é mudar o valor da variável primeiro. Pode-se ter uma lista maior de expressões ligadas por vírgulas, mas isso deve ser feito somente quando a ordem de avaliação não for importante. Se a ordem de avaliação for importante, você deve usar parênteses. Por exemplo: resultado = ((primeiro = 2, segundo = primeiro + 1), terceiro = segundo + 1);
estabelece o valor de resultado como sendo 4. Entretanto, o valor que a seguinte linha dá a resultado é imprevisível, porque não há garantias de que a expressão será avaliada em ordem: resultado = (primeiro = 2, segundo = primeiro + 1, terceiro = segundo + 1);
Por exemplo, terceiro = segundo + 1 pode ser avaliado antes de segundo = primeiro + 1.1 ■ COMANDO for
O terceiro e último comando loop em C++ é o comando for. O comando for é mais usado para se percorrer uma variável inteira em incrementos iguais. Como veremos no Capítulo 5, o comando for geralmente é utilizado para se percorrer um vetor. O comando for é, entretanto, um mecanismo geral de looping que pode fazer qualquer coisa que um loop while faça. Por exemplo, o seguinte comando for soma os inteiros de 1 a 10: soma = 0; for (n = 1; n <= 10; n++) soma = soma + n;
Um comando for começa com a palavra-chave for seguida por três expressões entre parênteses que dizem ao computador o que fazer com a variável de controle. O início de um comando for tem esta aparência: for ( Acao_De_Inicializacao ;
Expressao_Booleana ; Acao_De_Atualizacao )
A primeira expressão, Acao_De_Inicializacao , diz como a variável, variáveis ou outras coisas são inicializadas; a segunda, Expressao_Booleana fornece uma expressão booleana que é utilizada para verificar quando o loop deve terminar, e a última expressão, Acao_De_Atualizacao , diz como a variável de controle do loop é atualizada após cada iteração do corpo do loop. As três expressões no início de um comando for são separadas por dois — e apenas dois — ponto-e-vírgulas. Não caia na tentação de colocar um ponto-e-vírgula depois da terceira expressão. (A explicação técnica é que essas três coisas são expressões, não comandos, e não requerem um ponto-e-vírgula no final.) Um comando for em geral emprega uma única variável int para controlar a iteração e o final do loop. Entretanto, as três expressões no início de um comando for podem ser quaisquer expressões em C++; portanto, podem envolver mais (ou até menos) do que uma variável, e as variáveis podem ser de qualquer tipo. Utilizando o operador vírgula, você pode acrescentar múltiplas ações à primeira ou à última (mas normalmente não à segunda) das três expressões dentro dos parênteses. Por exemplo, você pode deslocar a inicialização da variável soma para dentro do loop for para obter a seguinte linha, que é equivalente ao código do comando for que mostramos anteriormente: for (soma
= 0, n = 1; n <= 10; n++) soma = soma + n;
Embora não o aconselhemos a fazer isso, porque não é tão fácil de ler, você pode deslocar todo o corpo do loop for para o terceiro item dentro dos parênteses. O comando for anterior é equivalente ao seguinte: for (soma
1.
= 0, n = 1; n <= 10; soma = soma + n, n++);
O padrão C++ especifica que as expressões unidas por vírgulas devem ser avaliadas da esquerda para a direita. Entretanto, nossa experiência revela que nem todos os computadores seguem o padrão a esse respeito.
Loops
49
O Painel 2.7 mostra a sintaxe para um comando for e também descreve a ação do comando for mostrando como traduzi-lo em um comando while equivalente. Observe que em um comando for, como no comando while correspondente, a condição de parada é testada antes da primeira iteração do loop. Assim, é possível ter um loop for cujo corpo é executado zero vezes. O corpo de um comando for pode ser, e em geral é, um comando composto, como no seguinte exemplo: for (numero
= 100; numero >= 0; numero--)
{ cout << numero << "garrafas de cerveja na prateleira.\n"; if (numero > 0) cout << "Pegue uma e coloque na roda.\n"; }
A primeira e a última expressões entre parênteses no início do comando for podem ser quaisquer expressões em C++ e, assim, podem envolver qualquer número de variáveis e ser de qualquer tipo. Em um comando for, uma variável pode ser declarada ao mesmo tempo em que é inicializada. Por exemplo: for (int n
= 1; n < 10; n++) cout << n << endl;
Pode haver variação no modo como os compiladores lidam com tais declarações dentro de um comando for. Isso será discutido no Capítulo 3, na subseção intitulada "Variáveis Declaradas em um Loop for". Seria bom evitar tais declarações dentro de um comando for até chegar ao Capítulo 3; apenas as mencionamos aqui para efeito de referência. Painel 2.7
SINTAXE
Comando for
DO
COMANDO
for
for ( Acao_De_Inicializacao ;
Expressao_Booleana ; Acao_De_Atualizacao )
Corpo_Do_Comando
EXEMPLO
for (numero
= 100; numero >= 0; numero--) cout << numero << "garrafas de cerveja na prateleira.\n";
SINTAXE
DO
LOOP while EQUIVALENTE
Acao_De_Inicializacao ; while (Expressao_Booleana ) { Corpo_Do_Comando Acao_De_Atualizacao ; }
EXEMPLO EQUIVALENTE numero = 100; while (numero
>= 0)
{
}
cout << numero << "garrafas de cerveja na prateleira.\n"; numero--;
DIÁLOGO PROGRAMA-USUÁRIO 100 garrafas de cerveja na prateleira. 99 garrafas de cerveja na prateleira. . . . 0 garrafas de cerveja na prateleira.
50
Fluxo de Controle
COMANDO
for
SINTAXE for
( Acao_De_Inicializacao; Expressao_Booleana; Acao_De_Atualizacao) Corpo_Do_Comando
EXEMPLO for (soma
= 0, n = 1; n <= 10; n++) soma = soma + n;
Veja o Painel 2.7 para uma explicação da ação do comando for.
LOOPS QUE SE REPETEM N VEZES Um comando for pode ser utilizado para produzir um loop que repete o corpo do loop um número predeterminado de vezes. Por exemplo, o seguinte corpo de loop repete seu corpo de loop três vezes: for (int
contagem = 1; contagem <= 3; contagem++) cout << "Hip, Hip, Hurra\n";
O corpo de um comando for não precisa fazer referência a uma variável de controle do loop, como a variá vel contagem.
PONTO-E-VÍRGULA EXTRA EM UM COMANDO
for
Normalmente não se coloca ponto-e-vírgula depois do parênteses no início de um loop for. Para ver o que acontece, considere o seguinte loop for: Ponto-e-vírgula problema for (int contagem = 1; contagem <= 10; contagem++); cout << "Olá\n";
Se você não notar o ponto-e-vírgula extra, vai esperar que esse loop for escreva Olá na tela dez vezes. Se você notar o ponto-e-vírgula, vai esperar que o compilador emita uma mensagem de erro. Nenhuma das duas coisas acontece. Se você inserir esse loop for em um programa completo, o compilador não irá reclamar. Se você executar o programa, apenas um Olá será escrito, em vez dos dez esperados. O que está acontecendo? Para responder a essa questão, precisamos de algumas informações. Uma forma de criar um comando em C++ é colocar um ponto-e-vírgula depois de alguma coisa. Se você colocar um ponto-e-vírgula depois de x++, altera a expressão x++
para o comando x++;
Se você colocar um ponto-e-vírgula depois de nada, criará um comando. Assim, o ponto-e-vírgula em si é um comando, que é chamado de comando vazio ou comando nulo. O comando vazio não executa nenhuma ação, mas continua sendo um comando. Portanto, a linha seguinte é um loop for completo e legítimo, cujo corpo é um comando vazio: for (int contagem
= 1; contagem <= 10; contagem++);
Esse loop for é, na realidade, iterado dez vezes, mas, como o corpo é o comando vazio, nada acontece quando o corpo é iterado. Esse loop não faz nada, e não faz nada dez vezes! O mesmo tipo de problema pode surgir com um loop while. Tenha o cuidado de não colocar um ponto-e vírgula depois de fechar os parênteses que encerram a expressão booleana no início de um loop while. Um loop do-while apresenta o problema oposto. Você deve se lembrar de sempre terminar um loop do-while com um ponto-e-vírgula.
LOOPS INFINITOS Um loop while, do-while ou for não termina enquanto a expressão booleana de controle não for verdadeira. Essa expressão booleana normalmente contém uma variável que será alterada pelo corpo do loop, e em geral o valor dessa variável é alterado de uma forma que pode acabar tornando a expressão booleana falsa e, assim, finalizar o loop. Entretanto, se você cometer um erro e escrever seu programa de modo que a expressão booleana seja sempre verdadeira, o loop será executado para sempre. Um loop que é executado para sempre é chamado de loop infinito . Infelizmente, exemplos de loops infinitos não são difíceis de encontrar. Primeiro vamos descrever um loop que é finalizado. O seguinte código C++ escreverá os números pares positivos inferiores a 12. Ou seja, dará como saída os números 2, 4, 6, 8 e 10, um em cada linha, e então o loop se encerrará.
Loops
x = 2; while (x
!= 12)
{ cout << x << endl; x = x + 2; }
O valor de x é incrementado em 2 a cada iteração do loop até chegar a 12. A essa altura, a expressão booleana após a palavra while não é mais verdadeira, então o loop se encerra. Agora suponha que você queira escrever os números ímpares inferiores a 12, em vez dos números pares. Você pode pensar, erroneamente, que só precisaria alterar o comando inicial para x = 1;
Mas esse erro criará um loop infinito. Porque o valor de x pula de 11 para 13, o valor de x nunca será igual a 12; assim, o loop jamais terminará. Esse tipo de problema é comum quando os loops são encerrados pela verificação de uma quantidade numérica utilizando-se == ou !=. Quando se lida com números, sempre é mais seguro testar passando um valor. Por exemplo, o comando seguinte funcionará bem como a primeira linha de nosso loop while: while (x
< 12)
Com essa alteração, x pode ser inicializado com qualquer número e o loop sempre terminará. Um programa que é um loop infinito será executado para sempre a não ser que forças externas o detenham. Como você agora pode escrever programas que contenham um loop infinito, é uma boa idéia aprender como forçar um programa a terminar. O método para forçar um programa a parar varia de sistema para sistema. A combinação de teclas Control-C terminará um programa em muitos sistemas. (Para teclar Control-C, segure a tecla Control enquanto pressiona a tecla C.) Em programas simples, um loop infinito é quase sempre um erro. Entretanto, alguns programas são escritos propositadamente para ser executados para sempre (em princípio), tal como o principal loop externo de um programa de reservas de passagens aéreas, que só fica pedindo mais reservas até que você desligue o com-
27. Qual é a saída do seguinte trecho (quando inserido em um programa completo)? for (int contagem = 1; contagem < 5; contagem++) cout << (2 * contagem) << " ";
28. Qual é a saída do seguinte trecho (quando inserido em um programa completo)? for
(int n = 10; n > 0; n = n - 2)
{ cout << "Olá "; cout << n << endl; }
29. Qual é a saída do seguinte trecho (quando inserido em um programa completo)? for (double amostra
= 2; amostra > 0; amostra = amostra - 0.5)
cout << amostra << " ";
30. Reescreva os seguintes loops como loops for. a. int i
= 1; while (i <= 10) { if (i < 5 && i != 2) cout << ’X’; i++; }
b. int i
= 1; <= 10)
while (i
{ cout << ’X’;
51
52
Fluxo de Controle
i = i + 3; }
c. long n = 100; do
{ cout << ’X’; n = n + 100; } while (n < 1000);
31. Qual é a saída deste loop? Identifique a conexão entre o valor de n e o valor da variável log. int n = 1024; int log = 0; for (int i = 1; i < n; i = i * 2)
log++; cout << n << " " << log << endl;
32. Qual é a saída deste loop? Comente a respeito do código. (Não é o mesmo do exercício anterior.) int n = 1024; int log = 0; for (int i = 1; i < n; i = i * 2);
log++; cout << n << " " << log << endl;
33. Qual é a saída deste loop? Comente a respeito do código. (Não é o mesmo dos dois exercícios anteriores.) int n = 1024; int log = 0; for (int i = 0; i < n; i = i * 2);
log++; cout << n << " " << log << endl;
34. Para cada uma das seguintes situações, diga qual tipo de loop (while, do-while ou for) funcionaria melhor. a. Soma de uma série, como 1/2 + 1/3 + 1/4 + 1/5 + ... + 1/10. b. Leitura da lista de notas de prova de um estudante. c. Leitura do número de dias de licença-saúde tirados pelos empregados de um departamento. d. Teste de uma função para verificar como ela funciona para diferentes valores de seus argumentos. 35. Qual é a saída produzida pelo seguinte trecho? (x é do tipo int.) int x = 10; while (x > 0)
{ cout << x << endl; x = x + 3; }
■ COMANDOS
break E continue
Nas subseções anteriores, descrevemos o fluxo de controle básico para os loops while, do-while e for. É assim que os loops normalmente devem ser e são usados. Entretanto, você pode alterar o fluxo de controle de duas formas, o que, em casos raros, pode ser uma técnica útil e segura. As duas formas de se alterar o fluxo de controle são inserir um comando break ou continue. O comando break encerra o loop. O comando continue encerra a iteração atual do corpo do loop. O comando break pode ser usado com qualquer um dos comandos loop de C++. Descrevemos o comando break quando discutirmos o comando switch. O comando break consiste na pala vra-chave break seguida por um ponto-e-vírgula. Quando executado, o comando break encerra o comando loop ou switch mais próximo em que está inserido. O Painel 2.8 contém um exemplo de um comando break que termina um loop quando dados de entrada inapropriados são introduzidos. O comando continue é formado pela palavra-chave continue seguida por um ponto-e-vírgula. Quando executado, o comando continue encerra a iteração atual do corpo do loop do comando loop mais próximo em que está inserido. O Painel 2.9 apresenta um exemplo de um loop que contém um comando continue.
Loops
53
Um ponto que deveria ser observado quando se usa o comando continue em um loop for é que o comando continue transfere o controle para a expressão atualizada. Assim, qualquer variável de controle de loop será atualizada imediatamente depois que o comando continue for executado. Observe que um comando break encerra completamente o loop. Em contraste, um comando continue apenas encerra uma iteração do loop; a próxima iteração (se houver) continua o loop. Você achará instrutivo comparar os detalhes dos programas no Painel 2.8 e 2.9. Preste atenção, especialmente, na mudança da expressão booleana de controle. Painel 2.8
Comando break em um loop
1 2
#include using namespace std;
3 4 5 6
int main( )
7 8 9
{ int numero, soma = 0, contagem = 0;
cout << "Digite 4 números negativos:\n"; while (++contagem <= 4)
{ cin >> number;
10 11 12 13 14 15 16 17 18
if (number >= 0)
{ cout << << << << << ; break
"ERRO: número positivo" " ou zero foi digitado na posição\n" contagem << " O último número digitado " "deve ser o da posição" << contagem << " O número da posição\n" contagem << "não foi acrescentado.\n";
}
19 20
soma = soma + numero; }
21 22
cout << sum << " é a soma dos primeiros " << (count - 1) << " números.\n";
23 24 }
return 0;
DIÁLOGO PROGRAMA-USUÁRIO Digite 4 números negativos: -1 -2 3 -4
ERRO: número positivo ou zero foi digitado na posição 3! O último número digitado deve ser o da posição 3. O número da posição 3 não foi acrescentado. -3 é a soma dos dois primeiros números.
Painel 2.9
1 2 3 4 5 6
Comando continue em um loop ( parte 1 de 2)
#include using namespace std; int main( )
{ int numero, soma = 0, contagem = 0;
cout << "Digite 4 números negativos, UM EM CADA LINHA:\n";
54
Fluxo de Controle
Painel 2.9
7 8 9
Comando
continue em
while (contagem
um loop ( parte 2 de 2)
< 4)
{ cin >> numero;
10 11 12 13 14 15
if
16 17 18
soma = soma + numero; contagem++;
(numero >= 0)
{ cout << "ERRO: número positivo (ou zero)!\n" << "Digite novamente esse número e continue:\n"; continue ; }
19 20 21 22 }
} cout << soma << "é a soma dos " << contagem << " números.\n"; return 0;
DIÁLOGO PROGRAMA-USUÁRIO Digite 4 números negativos, UM EM CADA LINHA: 1
ERRO: número positivo (ou zero)! Digite novamente esse número e continue: -1 -2 3
ERRO: número positivo! Digite novamente esse número e continue: -3 -4
-10 é a soma dos 4 números.
Observe que você não precisa obrigatoriamente de um comando break ou continue. Os programas dos Painéis 2.8 e 2.9 podem ser reescritos eliminando-se os comandos break e continue. O comando continue pode ser especialmente enganador e tornar seu código ilegível. É melhor evitar o comando continue completamente ou, ao menos, utilizá-lo apenas em raras ocasiões. ■ LOOPS ANINHADOS
É perfeitamente legal aninhar um loop dentro de outro. Quando fizer isso, lembre-se de que qualquer comando break ou continue se aplica ao loop (ou switch) mais interno que contenha o comando break ou continue. É melhor evitar loops aninhados, colocando o loop interno dentro de uma definição de função e uma invocação de função fora do loop externo. Falaremos sobre funções no Capítulo 3.
36. O que faz um comando break? Onde é correto colocar um comando break? 37. Preveja a saída dos seguintes loops aninhados: int n, m; for (n = 1; n <= 10; n++) for (m = 10; m >= 1; m--) cout << n << " vezes " << m << " = " << n * m << endl;
Respostas dos Exercícios de Autoteste
■ ■ ■
■ ■ ■
■ ■
55
As expressões booleanas são avaliadas de forma semelhante às expressões aritméticas. As estruturas de controle em C++ são o comando if-else e o comando switch. Um comando switch é uma estrutura de controle de seleções múltiplas. Também se podem formar estruturas de controle de seleções múltiplas aninhando-se comandos if-else para formar um comando if-else de seleções múltiplas. Um comando switch é uma boa forma de se implementar um menu para o usuário do seu programa. Os loops em C++ são os comandos while, do-while e for. Um comando do-while sempre itera seu corpo de loop pelo menos uma vez. Tanto o comando while quanto o comando for podem iterar seu corpo de loop zero vezes. Um loop for pode ser usado para obter o equivalente da instrução "repita o corpo do loop n vezes". Um loop pode ser interrompido por meio do comando break. Uma única iteração do corpo do loop pode ser interrompida por meio do comando continue. Não se deve exagerar no uso de comandos break. É melhor evitar comandos continue, embora alguns programadores os utilizem em raras ocasiões.
RESPOSTAS DOS EXERCÍCIOS DE AUTOTESTE
1. a. true. b. true. Observe que as expressões a e b querem dizer exatamente a mesma coisa. Como os operadores == e < têm maior precedência que &&, você não precisa incluir parênteses. Os parênteses, todavia, tornam o código mais legível. A maioria das pessoas acha a expressão a mais fácil de ler que a b , embora o significado seja o mesmo. c. true. d. true. e. false. Como o valor da primeira subexpressão, (contagem == 1), é false, você sabe que toda a expressão é false sem se preocupar em avaliar a segunda subexpressão. Assim, não importa o que são os valores x e y. Esta é a avaliação curto-circuito. f. true. Como o valor da primeira subexpressão, (contagem < 10), é true, você sabe que toda a expressão é true sem se preocupar em avaliar a segunda subexpressão. Assim, não importa o que são os valores x e y. Esta é a avaliação curto-circuito. g. false. Observe que a expressão em g inclui a expressão em f como uma subexpressão. Essa subexpressão é avaliada por meio da avaliação curto-circuito que descrevemos em f . Toda a expressão em g é equivalente a !( (true || (x < y)) && true )
h. i.
j. k.
que, por sua vez, é equivalente a !( true && true ), e que é equivalente a !(true), que, por sua vez, é equivalente ao valor final false. Essa expressão produz um erro quando é avaliada, porque a primeira subexpressão, ((limite/contagem) > 7), envolve uma divisão por zero. true. Como o valor da primeira expressão, (limite < 20) , é true, você sabe que a expressão inteira é true sem se importar em avaliar a segunda expressão. Assim, a segunda subexpressão ((limite/contagem) > 7), nunca é avaliada e, portanto, o fato de que envolve uma divisão por zero nunca é notado pelo computador. Trata-se de uma avaliação curto-circuito. Esta expressão produz um erro quando é avaliada porque a primeira subexpressão, ((limite/contagem) > 7), envolve uma divisão por zero. false. Como o valor da subexpressão, (limite < 0), é false, você sabe que a expressão inteira é false sem se preocupar em avaliar a segunda subexpressão. Assim, a segunda subexpressão, ((limite/contagem) > 7), nunca é avaliada e, portanto, o fato de que envolve uma divisão por zero nunca é notado pelo computador. Trata-se de uma avaliação curto-circuito.
56
Fluxo de Controle
l. Se você acha que esta expressão é absurda, tem razão. A expressão não possui nenhum significado intuitivo, mas o C++ converte os valores int para bool e avalia as operações && e !. Assim, o C++ avaliará essa bagunça! Lembre-se de que em C++ qualquer inteiro não-zero se converte em true, e 0 se converte em false. Logo, C++ avaliará (5 && 7) + (!6)
da seguinte forma: na expressão (5 && 7), 5 e 7 se convertem em true; true && true se convertem em true, que o C++ converte em 1. Na expressão (!6) 6 é convertido em true, e !(true) resulta em false, que o C++ converte em 0. Assim, a expressão inteira resulta em 1 + 0, que é 1. O valor final é 1. O C++ con verterá o número 1 em true, mas a resposta não possui grande significado intuitivo como true; talvez seja melhor dizer apenas que a resposta é 1. Você não precisa se especializar em avaliar expressões absurdas como esta, mas um pouco de treino ajudará a entender por que o compilador não fornece uma mensagem de erro quando você se engana e mistura operadores numéricos e booleanos em uma única expressão. 2. A expressão 2 < x < 3 é legal. Entretanto, não significa (2 < x) && (x < 3)
como muitos desejariam. Significa (2 < x) < 3. Como (2 < x) é uma expressão booleana, seu valor é true ou false, sendo convertido em 0 ou 1 e, portanto, menor que 3. Assim, 2 < x < 3 é sempre true. O resultado é true independentemente do valor de x. 3. (x < -1) || (x > 2) 4. (x > 1) && (x < 3) 5. Não. Na expressão booleana, (j > 0) é false (j acabou de receber a atribuição do valor -1). O && utiliza avaliação curto-circuito, que não avalia a segunda expressão se o resultado final puder ser determinado pela primeira expressão. A primeira expressão é false, então a segunda não tem importância. 6. if (pontos > 100) cout << "Alto"; else
cout << "Baixo";
Você pode querer acrescentar um \n ao final das strings entre aspas acima, dependendo de outros detalhes do programa. 7.
if (economias
>= despesas)
{ economias = economias - despesas; despesas = 0; cout << "Solvente"; } else
{ cout << "Falido"; }
Você pode querer acrescentar um \n ao final das strings entre aspas acima, dependendo de outros detalhes do programa. 8.
if (
(exame >= 60) && (programasFeitos >= 10) ) cout << "Aprovado";
else
cout << Reprovado";
Você pode querer acrescentar um \n ao final das strings entre aspas acima, dependendo de outros detalhes do programa. 9. if ( (temperatura >= 100) || (pressao >= 200) ) cout << "Alerta"; else
cout << "OK";
Você pode querer acrescentar um \n ao final das strings entre aspas acima, dependendo de outros detalhes do programa.
Respostas dos Exercícios de Autoteste
57
10. Todos os inteiros não-zero são convertidos em true; 0 é convertido em false. a. 0 é false b. 1 é true c. -1 é true 11. Início Olá do segundo if. Fim Início de novo Fim de novo grande pequeno médio
12. 13. 14. 15. Qualquer uma dessas duas formas está correta: if (n
< 0) cout << n << " else if ( (0 <= n) cout << n << " else if (n > 100) cout << n << "
é menor que zero.\n"; && (n <= 100) ) está entre 0 e 100 (inclusive).\n"; é maior que 100.\n";
e if (n
< 0) cout << n << " é menor que zero.\n"; else if (n <= 100) cout << n << " está entre 0 e 100 (inclusive).\n"; else
16. 17. 18. 19. 20. 21. 22.
cout << n << " é maior que 100.\n"; 3 2 1 0 2 1 7 5 2 1 0 2 1 1 2 3 4 1 2 3 10 7 4 1
23. Não haverá saída; o loop é iterado zero vezes. 24. 10 7 4 1 -42
25. 26. Com um comando do-while, o corpo do loop é sempre executado pelo menos uma vez. Com um comando while, pode haver condições em que o corpo do loop não é executado nenhuma vez. 27. 2 4 6 8 28. Olá 10
29. 30.
Olá 8 Olá 6 Olá 4 Olá 2 2.000000 1.500000 1.000000 0.500000 a for (int i = 1; i <= 10; i++) if (i < 5 && i != 2) cout << ’X’; .
58
Fluxo de Controle
b.
for (int i = 1; i <= 10; i = i + 3)
cout << ’X’; cout << ’X’; //Necessário para manter a mesma saída. Observe //também a mudança na inicialização de n for (long n = 200; n < 1000; n = n + 100) cout << ’X’; saída é 1024 10. O segundo número é o logaritmo de base 2 do primeiro
c.
31. A número. (Se o primeiro número não for uma potência de 2, então é produzida apenas uma aproximação do logaritmo de base 2.) 32. A saída é 1024 1. O ponto-e-vírgula depois da primeira linha do loop for provavelmente é uma armadilha, um erro. 33. Este é um loop infinito. Considere a expressão de atualização, i = i * 2. A expressão não pode alterar i, porque o valor inicial de i é 0. Não há saída, por causa do ponto-e-vírgula após a primeira linha do loop for. 34. a. Um loop for. b. e c. Ambos requerem um loop while, porque a lista de entrada pode estar vazia. d. Um loop do-while pode ser usado, porque pelo menos um teste será executado. 35. Este é um loop infinito. As primeiras linhas da saída serão assim: 10 13 16 19 21
36. Um comando break é usado para sair de um loop (um comando while, do-while ou for) ou para terminar um comando switch. Um comando break não é legal em nenhuma outra parte de um programa em C++. Note que, se os loops estiverem aninhados, o comando break encerra apenas um nível do loop. 37. A saída é muito longa para reproduzirmos aqui. O padrão é o seguinte: 1 vezes 10 = 10 1 vezes 9 = 9 . . . 1 vezes 1 = 1 2 vezes 10 = 20 2 vezes 9 = 18 . . . 2 vezes 1 = 2 3 vezes 10 = 30 . . .
PROJETOS DE PROGRAMAÇÃO 1. É difícil elaborar um orçamento que abranja vários anos, porque os preços não são estáveis. Se sua empresa necessita de 200 lápis por ano, você não pode simplesmente utilizar o preço dos lápis este ano para uma projeção para daqui dois anos. Devido à inflação, o custo provavelmente será maior do que é hoje. Escreva um programa para estimar o custo esperado de um item em um número especificado de anos. O programa pede o custo de cada item, o número de anos, a partir de agora, em que os itens serão adquiridos e a taxa de inflação. Então, o programa apresenta como saída o custo estimado de cada item após o período especificado. Faça com que o usuário informe a taxa de inflação como uma porcentagem, como, por exemplo, 5,6 (por cento). Seu programa deve converter a porcentagem em uma fração, como 0,056, e utilizar um loop para estimar o preço ajustado com a inflação. ( Dica: utilize um loop.)
Projetos de Programação
59
2. Você acaba de adquirir um aparelho estereofônico que custa R$ 1.000 por meio do seguinte plano de crediário: zero de entrada, juros de 18% ao ano (e, portanto, 1,5% ao mês) e prestações mensais de R$ 50. A prestação mensal de R$ 50 é utilizada para pagar os juros, e o restante é utilizado para pagar parte da dívida remanescente. Assim, no primeiro mês você paga 1,5% de R$ 1.000 em juros. Isso dá R$ 15. Os restantes R$ 35 são deduzidos do seu débito, o que o deixa com um débito de R$ 965,00. No mês seguinte você paga um juro de 1,5% sobre R$ 965,00, que dá R$ 14,48. Assim, você pode deduzir R$ 35,52 (que é R$ 50 – R$ 14,48) da soma que deve. Escreva um programa que lhe diga quantos meses você levará para pagar o que deve, assim como a soma total paga em juros. Utilize um loop para calcular a soma paga em juros e o tamanho do débito a cada mês. (Seu programa final não precisa fornecer a quantia paga mensalmente a título de juros, mas você pode querer escrever uma versão preliminar do programa que apresente esses valores.) Utilize uma variável para contar o número de iterações do loop e, portanto, o número de meses até que o débito seja zero. Você pode querer utilizar outras variáveis também. O último pagamento pode ser inferior a R$ 50 se o débito for menor, mas não se esqueça dos juros. Se você deve R$ 50, então sua prestação mensal de R$ 50 não saldará seu débito, embora vá chegar perto disso. Os juros de um mês sobre R$ 50 são de apenas 75 centavos.
Página em branco
Fundamentos das Funções Fundamentos das Funções
Capítulo 3Os Fundamentos das Funções Os melhores perfumes vêm nos menores frascos. Sabedoria popular
INTRODUÇÃO Se você já programou em alguma outra linguagem, o conteúdo deste capítulo lhe será bastante familiar. Mesmo assim, você deve dar uma olhada neste capítulo para ver a sintaxe e a terminologia de C++ para os fundamentos das funções. O Capítulo 4 contém o material sobre funções em C++ que pode ser diferente das outras linguagens. Pode-se considerar que um programa consiste em subpartes, como a obtenção dos dados de entrada, o cálculo dos dados de saída e a exibição dos dados de saída. C++, como a maioria das linguagens de programação, possui recursos para nomear e codificar cada uma dessas partes em separado. Em C++, essas subpartes são chamadas funções . A maioria das linguagens de programação possui funções ou algo similar, embora nem sempre sejam chamadas por esse nome. Os termos procedimento , subprograma e método , dos quais você já deve ter ouvido falar, significam essencialmente a mesma coisa que função . Em C++, uma função pode retornar um valor (produzir um valor) ou pode executar alguma ação sem retornar um valor, mas, quer a subparte forneça um valor ou não, ainda é chamada de função em C++. Este capítulo apresenta os detalhes básicos sobre as funções em C++. Antes de lhe dizer como escrever suas próprias funções, vamos lhe contar como utilizar algumas das funções predefinidas do C++.
3.1
Funções Predefinidas Não reinvente a roda. Sabedoria popular
O C++ vem com bibliotecas de funções predefinidas que você pode utilizar em seus programas. Existem dois tipos de funções em C++; funções que retornam (produzem) um valor e funções que não retornam um valor. Funções que não retornam um valor são chamadas de funções void. Primeiro falaremos das funções que retornam um valor, e depois das funções void. ■
FUNÇÕES PREDEFINIDAS QUE RETORNAM UM VALOR
Vamos usar a função sqrt para ilustrar como se utiliza uma função predefinida que retorna um valor. A função sqrt calcula a raiz quadrada de um número. (A raiz quadrada de um número é aquele número que, quando multiplicado por si mesmo, produzirá o número com o qual você começou. Por exemplo, a raiz quadrada de 9 é 3, porque 3 2 é igual a 9.) A função sqrt começa com um número, como 9.0, e calcula sua raiz quadrada, no caso 3.0. O valor da função começa com o que é chamado seu argumento. O va-
62
Fundamentos das Funções
lor que ela calcula é chamado de valor retornado. Algumas funções podem ter mais de um argumento, mas nenhuma função pode ter mais de um valor retornado. A sintaxe para utilizar funções em seu programa é simples. Para estabelecer que uma variável chamada aRaiz é igual à raiz quadrada de 9.0, você pode utilizar a seguinte declaração de atribuição: aRaiz = sqrt(9.0);
A expressão sqrt(9.0) é conhecida como uma chamada de função ou invocação de função. Um argumento em uma função pode ser uma constante, como 9.0, uma variável, ou uma expressão mais complicada. Uma chamada de função é uma expressão que pode ser usada como qualquer outra expressão. Por exemplo, o valor retornado por sqrt é do tipo double; portanto, a linha seguinte é legal (embora talvez muito restrita): bonus = sqrt(vendas)/10;
vendas e bonus são variáveis que normalmente seriam do tipo double. A chamada de função sqrt(vendas) é um item único, como se estivesse entre parênteses. Assim, a declaração de atribuição acima é equivalente a bonus = (sqrt(vendas))/10;
Você pode utilizar uma chamada de função onde seja legal utilizar uma expressão do tipo especificado pelo valor retornado pela função. O Painel 3.1 contém um programa completo que utiliza a função predefinida sqrt. O programa calcula o tamanho da maior casinha de cachorro que pode ser construída com a quantidade de dinheiro que o usuário está disposto a gastar. O programa pede ao usuário uma quantia e, então, determina quantos pés * quadrados de área podem ser adquiridos com essa quantia. O cálculo fornece a área da casinha de cachorro em pés quadrados. A função sqrt fornece o comprimento de um lado do piso da casinha. A biblioteca cmath contém a definição da função sqrt e diversas outras funções matemáticas. Se seu programa utiliza uma função predefinida de alguma biblioteca, deve conter uma instrução de include que dê nome a essa biblioteca. Por exemplo, o programa no Painel 3.1 utiliza a função sqrt e assim contém #include
Este programa em particular possui duas instruções de include. Não importa em que ordem estejam essas instruções. As instruções de include foram discutidas no Capítulo 1. As definições para funções predefinidas normalmente colocam essas funções no ambiente de nomes std e também requerem a seguinte instrução de using, como ilustrado no Painel 3.1: using namespace std; Painel 3.1
1 2 3 4 5 6 7 8 9
Função predefinida que retorna um valor ( parte 1 de 2) //Calcula o tamanho de uma casinha de cachorro que possa ser adquirida //dado o orçamento do usuário. #include using namespace std; int main( )
{ const double COST_PER_SQ_FT = 10.50; double orcamento, area, comprimentoLado;
10 11
cout << "Informe quanto quer gastar com a casinha de cachorro $"; cin >> budget;
12 13
area = budget/COST_PER_SQ_FT; lengthSide = sqrt(area);
14 15
cout.setf(ios::fixed); cout.setf(ios::showpoint);
*
Um pé equivale a 30,48 cm. (N. do R.T.)
Funções Predefinidas Painel 3.1
63
Função predefinida que retorna um valor ( parte 2 de 2)
16 17 18 19 20
cout.precision(2); cout << "Por um preço de $" << budget << endl << "Posso lhe construir uma magnífica casinha de cachorro\n" << "com " << lengthSide << " pés em cada lado.\n";
21 22 }
return 0;
DIÁLOGO PROGRAMA-USUÁRIO Informe quanto quer gastar com a casinha de cachorro $25.00 Por um preço de $25.00 Posso construir uma magnífica casinha de cachorro com 1.54 pés em cada lado.
Normalmente, tudo o que você precisa fazer para utilizar uma biblioteca é colocar uma instrução de include e outra de using para aquela biblioteca no arquivo com seu programa. Se tudo funcionar somente com essas instruções, você não precisa se preocupar em fazer mais nada. Entretanto, para algumas bibliotecas em alguns sistemas você precisa fornecer instruções adicionais para o compilador ou executar explicitamente um programa de ligação (linker ) para estabelecer a ligação com a biblioteca. Os detalhes variam de um sistema para outro; você terá de verificar seu manual ou consultar um especialista para ver exatamente o que é necessário.
FUNÇÕES QUE RETORNAM UM VALOR Para uma função que retorna um valor, uma chamada de função é uma expressão que consiste no nome da função seguido por argumentos entre parênteses. Se houver mais de um argumento, os argumentos são separados por vírgulas. Se a chamada da função retornar um valor, então a chamada da função é uma expressão que pode ser usada como qualquer outra expressão do tipo especificado para o valor retornado pela função.
SINTAXE Nome_Da_Funcao(Lista_De_Argumentos) em que Lista_De_Argumentos é uma lista de argumentos separados por vírgulas: Argumento_1, Argumento_2, . . . , Argumento_Final
EXEMPLOS lado = sqrt(area); cout << "2.5 elevado a 3.0 é " << pow(2.5, 3.0);
Algumas funções predefinidas são descritas no Painel 3.2. Mais funções predefinidas são descritas no Apêndice 4. Observe que as funções de valor absoluto abs e labs estão na biblioteca com o arquivo de cabeçalho cstdlib, assim qualquer programa que utilizar qualquer uma dessas funções deve conter a seguinte instrução: #include
Observe também que existem três funções de valor absoluto. Se você quiser produzir o valor absoluto de um número de tipo int, utilize abs; se quiser produzir o valor absoluto de um número do tipo long, utilize labs; e se quiser produzir o valor absoluto de um número de tipo double, utilize fabs. Para complicar ainda mais as coisas, abs e labs estão na biblioteca com o arquivo de cabeçalho cstdlib, enquanto fabs está na biblioteca com o arquivo de cabeçalho cmath. fabs é uma abreviação de floating-point absolute value (valor absoluto de ponto flutuante). Lembre-se de que os números com uma fração após o ponto decimal (ou vírgula decimal), como os números de tipo double, geralmente são chamados de números de ponto flutuante . Outro exemplo de uma função predefinida é pow, que está na biblioteca com o arquivo de cabeçalho cmath. A função pow pode ser usada para efetuar a potenciação em C++. Por exemplo, se você quiser fixar uma variável resultado como igual a x y, pode utilizar a forma: resultado = pow (x, y);
64
Fundamentos das Funções
Assim, as três linhas seguintes de código apresentarão como saída na tela o número 9.0, porque (3.0)2.0 é 9.0: double resultado, x = 3.0, y = 2.0;
resultado = pow(x, y); cout << resultado; Painel 3.2
Algumas funções predefinidas
NOME
DESCRIÇÃO
TIPO DE ARGUMENTOS
TIPO OU VALOR RETORNADO
EXEMPLO
VALOR
BIBLIOTECA HEADER
sqrt
Raiz quadrada
double
double
sqrt(4.0)
2.0
cmath
pow
Potência
double
double
pow(2.0,3.0)
8.0
cmath
abs
Valor absoluto para int
int
int
abs(-7) abs(7)
7 7
cstdlib
labs
Valor absoluto para long
long
long
labs(-70000) labs(70000)
70000 70000
cstdlib
fabs
Valor absoluto para double
double
double
fabs(-7.5) fabs(7.5)
7.5 7.5
cmath
ceil
Ceiling (arredonda para próximo inteiro)
double
double
ceil(3.2) ceil(3.9)
4.0 4.0
cmath
floor
Floor (arredonda para inteiro anterior)
double
double
floor(3.2) floor(3.9)
3.0 3.0
cmath
exit
Finaliza o programa
int
void
exit(1);
Nenhum
cstdlib
rand
Número aleatório
Nenhum
int
rand( )
Varia
cstdlib
srand
Estabelece a semente para rand
unsi gned i nt
voi d
srand(42);
Nenhum
cstdlib
Todas essas funções predefinidas requerem using namespace std, além de uma instrução de include. Observe que a chamada anterior a pow retorna 9.0, não 9. A função pow sempre retorna um valor de tipo double, não de tipo int. Observe também que a função pow requer dois argumentos. Uma função pode ter qualquer número de argumentos. Além disso, toda posição de argumento possui um tipo específico, e o argumento utilizado em uma chamada de função deve ser desse tipo. Em muitos casos, se você utiliza um argumento de tipo errado, o C++ realizará conversões automáticas de tipo para você. Entretanto, os resultados podem não ser os que você esperava. Quando se chama uma função, devem-se usar argumentos do tipo específico para aquela função. Uma exceção a isso é a conversão automática de argumentos do tipo int para o tipo double. Em muitas situações, inclusive chamadas à função pow, pode-se utilizar com segurança um argumento do tipo int (ou outro tipo inteiro) quando um argumento de tipo double (ou outro tipo de ponto flutuante) é especificado. FUNÇÕES void Uma função void executa alguma ação, mas não retorna um valor. Para uma função void, uma chamada de função é um comando formado pelo nome da função seguido por argumentos entre parênteses e terminado por um ponto-e-vírgula. Se houver mais de um argumento, os argumentos são separados por vírgulas. Para uma função void, uma invocação de função (chamada de função) é um comando que pode ser usado como qualquer outro comando em C++.
SINTAXE Nome_Da_Funcao(Lista_De_Argumentos) em que Lista_De_Argumentos é uma lista de argumentos separados por vírgulas: Argumento_1, Argumento_2, . . . , Argumento_Final
EXEMPLO exit(1);
Funções Predefinidas
65
Muitas implementações de pow possuem uma restrição quanto aos argumentos que podem ser utilizados. Nessas implementações, se o primeiro argumento de pow é negativo, o segundo argumento deve ser um número inteiro. Pode ser mais fácil e seguro utilizar pow apenas quando o primeiro argumento é não-negativo. ■
FUNÇÕES void PREDEFINIDAS
Uma função void executa alguma ação, mas não retorna um valor. Como executa uma ação, uma invocação de função void é um comando. A chamada de função para uma função void é escrita de maneira similar à de uma chamada de função para uma função que retorna um valor, a não ser pelo fato de ser terminada por um ponto-e-vírgula e usada como um comando e não como uma expressão. Funções void predefinidas são tratadas da mesma forma que as funções predefinidas que retornam um valor. Assim, para utilizar uma função void predefinida, seu programa deve ter uma instrução de include que dê o nome da biblioteca que define a função. Por exemplo, a função exit é definida na biblioteca cstdlib e, assim, um programa que utiliza essa função deve conter as seguintes linhas no início (ou próximo do início) do arquivo: #include using namespace std;
A linha seguinte é uma amostra de invocação (amostra de chamada) da função exit: exit(1);
FUNÇÃO exit A função exit é uma função void predefinida que requer um argumento de tipo int. Assim, uma invocação à função exit é um comando escrito da seguinte forma: exit(Valor_Inteiro); Quando a função exit é invocada (ou seja, quando o comando acima é executado), o programa termina imediatamente. Qualquer Valor_Inteiro pode ser usado, mas, por convenção, 1 é usado para uma chamada a exit que seja provocada por um erro, e 0 é usado em outros casos. A definição da função exit está na biblioteca cstdlib e coloca a função exit no ambiente de nomes std (namespace std). Portanto, qualquer programa que utilizar a função exit deve conter as seguintes instruções: #include using namespace std;
Uma invocação à função exit encerra o programa imediatamente. O Painel 3.3 contém um programa que demonstra a função exit.
Painel 3.3
Chamada de função para uma função void predefinida ( parte 1 de 2)
1 2 3
#include #include using namespace std;
4 5 6 7
int main( )
Este é apenas um exemplo fictício. Produziria o mesmo efeito se você omitisse estas linhas.
{ cout << "Olá. Fora!\n"; exit(1);
8 9 10
cout << "Este comando é inútil,\n" << "porque nunca será executado.\n" << "Isto é só um programa fictício para exemplificar exit.\n";
11 12 }
return 0;
66
Fundamentos das Funções
Painel 3.3
Chamada de função para uma função
void predefinida
( parte 2 de 2)
DIÁLOGO PROGRAMA-USUÁRIO Olá. Fora!
Observe que a função exit possui um argumento, que é de tipo int. O argumento é fornecido para o sistema operacional. No que se refere ao seu programa em C++, você pode utilizar qualquer valor int como argumento, mas, por con venção, 1 é utilizado para uma chamada a exit que seja provocada por um erro, e 0 é utilizado nos outros casos. Uma função void pode ter qualquer número de argumentos. Os detalhes a respeito dos argumentos para funções void são os mesmos que para as funções que retornam um valor. Em particular, se você utilizar um argumento do tipo errado, em muitos casos o C++ realizará a conversão automática de tipos para você. Entretanto, os resultados podem não ser os que você esperava.
1. Determine o valor de cada uma das seguintes expressões aritméticas. sqrt(16.0) sqrt(16) pow(2.0, 3.0) pow(2, 3) pow(2.0, 3) pow(1.1, 2) abs(3) abs(-3) abs(0) fabs(-3.0) fabs(-3.5) fabs(3.5) ceil(5.1) ceil(5.8) floor(5.1) floor(5.8) pow(3.0, 2)/2.0 pow(3.0, 2)/2 7/abs(-2) (7 + sqrt(4.0))/3.0 sqrt(pow(3, 2)) 2. Converta cada uma das seguintes expressões matemáticas em uma expressão aritmética em C++. a. √ x y area + fudge + b. x y + 7 c.√ d. √ time + tide
√ b2 −4 ac
f. | x − y | 2a 3. Escreva um programa completo em C++ para calcular e apresentar como saída a raiz quadrada dos números inteiros de 1 a 10. 4. Qual é a função do argumento int à função void exit ? nobody
■
e.−b +
GERADOR DE NÚMEROS ALEATÓRIOS
Gerador de números aleatórios é uma função que retorna um número "aleatoriamente escolhido". É diferente das funções que já vimos até agora, no sentido de que o valor retornado não é determinado por argumentos (que normalmente não existem), e sim por algumas condições globais. Como é possível pensar-se no valor retornado como sendo um número aleatório, pode-se utilizar um gerador de números aleatórios para simular eventos aleatórios, como o resultado do lançamento de um dado ou moeda. Além de simular jogos de azar, os geradores de números aleatórios podem ser usados para simular coisas que, estritamente falando, podem não ser aleatórias, mas que parecem ser, como o intervalo de tempo entre a chegada de carros em um posto de pedágio. A biblioteca C++ com o arquivo de cabeçalho contém uma função de números aleatórios chamada rand. Essa função não possui argumentos. Quando seu programa invoca rand, a função retorna um inteiro no intervalo entre 0 e RAND_MAX, inclusive. (O número gerado pode ser igual a 0 ou a RAND_MAX.) RAND_MAX é uma constante inteira definida cuja definição também está na biblioteca com o arquivo de cabeçalho . O valor exato de RAND_MAX depende do sistema, mas será no mínimo 32767 (o máximo inteiro positivo de dois bytes). Por exemplo, as linhas seguintes apresentam como saída uma lista de dez números "aleatórios" no intervalo entre 0 e RAND_MAX: int i; for (i
= 0; i < 10; i++) cout << rand( ) << endl;
É mais provável que você queira um número aleatório em algum intervalo menor, como o intervalo entre 0 e 10. Para assegurar que o valor esteja no intervalo entre 0 e 10 (incluindo os extremos), você pode usar rand( ) % 11
Funções Predefinidas
67
Isso é chamado de ajuste de escala scaling (colocar em escala). * As seguintes linhas apresentam como saída dez inteiros "aleatórios" no intervalo entre 0 e 10 (inclusive): int i; for (i = 0; i < 10; i++)
cout << (rand( ) % 11) << endl;
Geradores de números aleatórios, como a função rand, não geram números verdadeiramente aleatórios. (Por isso as aspas que utilizamos em "aleatório".) Uma seqüência de chamadas à função rand (ou a quase todos os geradores de números aleatórios) produzirá uma seqüência de números (os valores retornados por rand) que parecem ser aleatórios. Entretanto, se você pudesse fazer com que o computador voltasse ao estado anterior, quando a seqüência de chamadas a rand se iniciou, você obteria a mesma seqüência de "números aleatórios". Números que parecem ser aleatórios, mas que, na realidade, não são, como uma seqüência de números gerada por chamadas a rand, são chamados de números pseudo-aleatórios. Uma seqüência de números pseudo-aleatórios geralmente é determinada por um número conhecido como semente. Se você iniciar o gerador de números aleatórios com a mesma semente, todas as vezes a mesma seqüência (aparentemente aleatória) de números será produzida. Você pode utilizar a função srand para fixar a semente para a função rand. A função void srand requer um argumento inteiro (positivo), que é a semente. Por exemplo, as linhas seguintes apresentarão como saída duas seqüências idênticas de dez números pseudo-aleatórios: int i;
srand(99); for (i = 0; cout << srand(99); for (i = 0; cout <<
i < 10; i++) (rand( ) % 11) << endl; i < 10; i++) (rand( ) % 11) << endl;
Não há nada de especial com o número 99, fora o fato de havermos utilizado o mesmo número para ambas as chamadas a srand. Observe que a seqüência de números pseudo-aleatórios produzida por uma determinada semente pode depender do sistema. Caso seja reexecutada em um sistema diferente com a mesma semente, a seqüência de números pseudo-aleatórios pode ser diferente nesse sistema. Entretanto, desde que você esteja no mesmo sistema utilizando a mesma implementação de C++, a mesma semente produzirá a mesma seqüência de números pseudo-aleatórios. NÚMEROS PSEUDO-ALEATÓRIOS A função rand não requer argumentos e retorna um inteiro pseudo-aleatório no intervalo entre 0 e RAND_MAX (inclusive). A função void srand requer um argumento, que é a semente para o gerador de números aleatórios rand. O argumento à srand é do tipo unsigned int, então o argumento deve ser não-negativo. As funções rand e srand , assim como a constante definida RAND_MAX, são definidas na biblioteca cstdlib, e os programas que as utilizam devem conter as seguintes instruções: #include using namespace std;
Esses números pseudo-aleatórios são suficientemente próximos de números aleatórios verdadeiros para a maioria das aplicações. Na realidade, muitas vezes eles são preferíveis aos números aleatórios verdadeiros. Um gerador de números pseudo-aleatórios possui uma grande vantagem em relação a um gerador de números aleatórios verdadeiros: a seqüência de números que ele produz pode ser repetida. Se executado duas vezes com o mesmo valor de semente, produzirá a mesma seqüência de números. Isso pode ser muito útil, em muitos casos. Quando um erro é descoberto e consertado, o programa pode ser reexecutado com a mesma seqüência de números pseudo-aleatórios que apresentaram o erro. De forma similar, uma execução particularmente interessante do programa pode ser repetida, desde que um gerador de números pseudo-aleatórios seja utilizado. Com um gerador de números verdadeiramente aleatórios cada execução do programa provavelmente será diferente.
*
O número 11 é chamado de fator de escala. (N. do R.T.)
68
Fundamentos das Funções
O Painel 3.4 mostra um programa que utiliza o gerador de números aleatórios para "prever" o clima. Nesse caso, a previsão é aleatória, mas algumas pessoas a consideram tão boa quanto qualquer previsão meteorológica. (As previsões meteorológicas podem, na realidade, ser bastante precisas, mas este programa é apenas um jogo para ilustrar os números pseudo-aleatórios.) Observe que, no Painel 3.4, o valor da semente usado para o argumento de srand é o mês multiplicado pelo dia. Assim, se o programa é reexecutado e a mesma data é fornecida, a mesma previsão será dada. (Claro que se trata de um programa bastante simples. A previsão para o dia depois do dia 14 pode ou não ser a mesma que a do dia 15, mas esse programa serve como um exemplo simples.) Probabilidades geralmente são representadas como um número de ponto flutuante entre 0.0 e 1.0. Suponha que você queira uma probabilidade aleatória em vez de um inteiro aleatório. Isso pode ser produzido por outra forma de ajuste de escala. A linha seguinte gera um valor de ponto flutuante pseudo-aleatório entre 0.0 e 1.0: rand( )/static_cast (RAND_MAX)
A conversão ( cast ) de tipo é feita para que obtenhamos uma divisão de ponto flutuante em vez de uma divisão de inteiros. Painel 3.4
1 2 3
Função utilizando um gerador de número aleatório (parte 1 de 2) #include #include using namespace std;
4 int main( ) 5 { int month, day; 6 7 cout << "Bem-vindo ao seu programa de previsão do tempo.\n" 8 << "Informe a data de hoje, utilizando dois inteiros para o mês e para o dia:\n"; 9 cin >> month; 10 cin >> day; 11 srand(month*day); int prediction; 12 char ans; 13 14 cout << "Previsão para hoje:\n"; do 15 16 { 17 prediction = rand( ) % 3; switch (prediction) 18 19 { case 0: 20 21 cout << "O dia será ensolarado!!\n"; 22 break; case 1: 23 24 cout << "O dia será nublado.\n"; 25 break; case 2: 26 27 cout << "Vai haver fortes chuvas!\n"; 28 break; default: 29 30 cout << "Programa de previsão do tempo não está funcionando corretamente.\n"; 31 } 32 cout << "Quer a previsão para o dia seguinte?(y/n): "; 33 cin >> ans; 34 } while (ans == ’y’ || ans == ’Y’); 35 cout << "Este é o final do seu programa de previsão do tempo de 24 horas.\n"; return 0; 36 37 }
Funções Definidas pelo Programador
Painel 3.4
69
Função utilizando um gerador de número aleatório (parte 2 de 2)
DIÁLOGO PROGRAMA-USUÁRIO Bem-vindo ao seu programa de previsão do tempo. Informe a data de hoje utilizando dois inteiros para o mês e para o dia: 2 14
Previsão para hoje: O dia será nublado. Quer a previsão para o dia seguinte? (s/n): s O dia será nublado. Quer a previsão para o dia seguinte? (s/n): s Vai haver fortes chuvas! Quer a previsão para o dia seguinte? (s/n): s Vai haver fortes chuvas! Quer a previsão para o dia seguinte? (s/n): s O dia será ensolarado!! Quer a previsão para o dia seguinte? (s/n): n Este é o final do seu programa de previsão do tempo de 24 horas.
5. Forneça uma expressão para produzir um número inteiro pseudo-aleatório no intervalo entre 5 e 10 (inclusive). 6. Escreva um programa completo que peça ao usuário uma semente e depois apresente uma lista de dez números aleatórios baseados nessa semente. Os números devem ser de ponto flutuante no intervalo entre 0.0 e 1.0 (inclusive).
3.2
Funções Definidas pelo Programador Um terno feito sob medida sempre cai melhor do que um de marca. Meu tio, alfaiate
A seção anterior explicou como utilizar funções predefinidas. Esta seção lhe dirá como definir suas próprias funções. ■
DEFININDO FUNÇÕES QUE RETORNAM UM VALOR
Você pode definir suas próprias funções, no mesmo arquivo da parte principal do seu programa ( main) ou em um arquivo separado, de modo que as funções possam ser utilizadas por vários programas diferentes. A definição é a mesma em ambos os casos, mas por enquanto vamos assumir que a definição da função esteja no mesmo arquivo que a parte main do seu programa. Esta subseção trata apenas de funções que retornam um valor. Uma subseção posterior lhe dirá como definir funções void. O Painel 3.5 contém uma amostra de definição de função que é um programa completo demonstrando uma chamada à função. A função se chama custoTotal e requer dois argumentos — o preço de um item e o número de itens adquiridos. A função retorna o custo total, incluindo imposto sobre vendas, para todos os itens com o preço especificado. A função é chamada da mesma forma que uma função predefinida. A definição da função que o programador deve escrever é um pouco mais complicada. A descrição da função é fornecida em duas partes. A primeira parte é chamada declaração de função ou protótipo de função. A linha seguinte é a declaração de função (protótipo de função) da função definida no Painel 3.5: double custoTotal(int numeroParametro, double precoParametro);
70
Fundamentos das Funções
A primeira palavra em uma declaração de função especifica o tipo do valor retornado pela função. Assim, para a função custoTotal, o tipo do valor retornado é double. A seguir, a declaração de função diz a você o nome da função; nesse caso, custoTotal. A declaração de função diz a você (e ao compilador) tudo o que você precisa saber para escrever e utilizar uma chamada de função. Diz a você quantos argumentos a função requer e de que tipo; nesse caso, a função custoTotal requer dois argumentos, o primeiro de tipo int e o segundo de tipo double. Os identificadores numeroParametro e precoParametro são chamados de parâmetros formais, ou simplesmente parâmetros. Um parâmetro formal é utilizado como um tipo de espaço em branco, ou "guardador" de lugar, para ficar no lugar do argumento. Quando escreve uma declaração de função, você não sabe o que vão ser os argumentos, então utiliza os parâmetros formais no lugar dos argumentos. Nomes de parâmetros formais podem ser quaisquer identificadores válidos. Observe que uma declaração de função termina com um ponto-e-vírgula. Embora a declaração de função lhe revele tudo o que precisa saber para escrever uma chamada de função, não lhe conta que valor será retornado como saída. O valor retornado é determinado pela definição de função. No Painel 3.3 a definição de função está nas linhas 2 a 30 do programa. Uma definição de função descreve como a função calcula o valor que retorna como saída. Uma definição de função consiste em um cabeçalho de função seguido por um corpo de função . Um cabeçalho de função é escrito de forma similar à declaração de função, a não ser pelo fato de o cabeçalho não ter um ponto-e-vírgula no final. O valor retornado é determinado pelos comandos no corpo da função . O corpo da função segue o cabeçalho e completa a definição da função. O corpo da função consiste em declarações e comandos executáveis entre chaves. Assim, o corpo da função é exatamente como o corpo da parte main de um programa. Quando uma função é chamada, os valores do argumento são conectados aos parâmetros formais e os comandos do corpo da função são executados. O valor retornado pela função é determinado quando a função executa um comando return. (Os detalhes dessa "conexão" serão discutidos no Capítulo 4.) Painel 3.5
Função utilizando um gerador de número aleatório ( parte 1 de 2)
1 2
#include using namespace std;
3 4 5
double totalCost(int numberParameter, double priceParameter); //Calcula o custo total, inclusive 5% de imposto sobre a venda, //em numberParameter itens a um custo de priceParameter cada.
6 7 8 9
int main( )
{ double price, bill; int number;
10 11 12 13
cout << "Informe o número de itens adquiridos: "; cin >> number; cout << "Informe o preço por item $"; cin >> price;
14
bill = totalCost(number, price);
15 16 17 18 19 20 21
cout.setf(ios::fixed); cout.setf(ios::showpoint); cout.precision(2); cout << number << " itens a " << "$" << price << " cada.\n" << "A soma final, incluindo impostos, é $" << bill << endl;
22 23 }
return 0;
Chamada de função
Declaração de função; também chamada de protótipo de função.
Funções Definidas pelo Programador
Painel 3.5
71
Função utilizando um gerador de número aleatório ( parte 2 de 2) Cabeçalho de função
24 25 26 27 28 29 30
double totalCost(int numberParameter, double priceParameter)
{ const double TAXRATE = 0.05; //5% de imposto sobre a venda double subtotal;
Corpo da função
Definição de função
subtotal = priceParameter * numberParameter; return (subtotal + subtotal*TAXRATE);
}
DIÁLOGO PROGRAMA-USUÁRIO Informe o número de itens adquiridos: 2 Informe o preço por item: $10.10 2 itens a $10.10 cada. A soma final, incluindo impostos, é $21.21
Um comando return é formado pela palavra-chave return seguida por uma expressão. A definição de função no Painel 3.5 contém o seguinte comando return: return (subtotal + subtotal*IMPOSTO)
Quando esse comando return é executado, o valor da seguinte expressão é retornado como o valor da chamada de função: (subtotal + subtotal*IMPOSTO)
Os parênteses não são necessários. O programa será executado da mesma forma se os parênteses forem omitidos. Entretanto, com expressões mais longas, os parênteses tornam o comando return mais legível. Para obter maior consistência, alguns programadores aconselham o uso desses parênteses mesmo com expressões simples. Na definição de função no Painel 3.3 não há nenhum comando após o comando return, mas, se houvesse, o comando não seria executado. Quando um comando return é executado, a chamada de função termina. Observe que o corpo da função pode conter quaisquer comandos em C++ e que os comandos serão executados quando a função for chamada. Assim, uma função que retorna um valor pode executar qualquer outra ação além de retornar um valor. Na maioria dos casos, todavia, o principal objetivo de uma função que retorna um valor é retornar esse valor. Ou a definição de função completa ou a declaração de função (protótipo de função) devem aparecer no código antes que a função seja chamada. O mais comum é a declaração de função e a parte main do programa aparecerem em um ou mais arquivos, com a declaração de função antes da parte main, e a definição da função aparecer em outro arquivo. Não abordamos ainda a questão da divisão de um programa em mais de um arquivo, e por isso colocaremos as definições de função depois da parte main do programa. Se a definição de função completa for colocada antes da parte main do programa, a declaração da função pode ser omitida. ■
FORMA ALTERNATIVA PARA DECLARAÇÕES DE FUNÇÃO
Não é preciso listar os nomes de parâmetros formais em uma declaração de função (protótipo de função). As duas declarações de função seguintes são equivalentes: double custoTotal(int numeroParametro, double precoParametro);
e double custoTotal(int, double);
Normalmente utilizamos a primeira forma para nos referir aos parâmetros formais no comentário que acompanha a declaração de função. Entretanto, muitas vezes você encontrará a segunda forma em manuais.
72
Fundamentos das Funções
Esta forma alternativa se aplica apenas a declarações de função. Uma definição de função deve sempre listar os nomes dos parâmetros formais. ARGUMENTOS NA ORDEM ERRADA Quando uma função é chamada, o computador substitui o primeiro parâmetro formal pelo primeiro argumento, o segundo parâmetro formal pelo segundo argumento e assim por diante. Embora o computador verifique o tipo de cada argumento, ele não verifica a coerência. Se você confundir a ordem dos argumentos, o programa não vai saber o que você pretendeu fazer. Se houver uma violação de tipo devido a um argumento de tipo errado, você receberá uma mensagem de erro. Se não houver violação de tipo, seu programa pro vavelmente será executado normalmente, mas o valor retornado pela função será incorreto.
USO DOS TERMOS P ARÂMETRO E ARGUMENTO O uso dos termos parâmetro formal e argumento que adotamos neste livro é consistente com o uso comum, mas muitas vezes os termos parâmetro e argumento são utilizados de forma invertida. Quando vir os termos parâmetro e argumento, você deve determinar seu significado exato a partir do contexto. Muitas pessoas utilizam o termo parâmetro tanto para o que chamamos de parâmetros formais quanto para o que chamamos de argumentos. Outras adotam o termo argumento tanto para o que chamamos de parâmetros formais quanto para o que chamamos argumentos. Não espere consistência na forma como as pessoas utilizam esses termos. (Neste livro, às vezes empregamos o termo parâmetro significando parâmetro formal, mas isto é mais uma abreviação do que uma verdadeira inconsistência.)
■
FUNÇÕES CHAMANDO FUNÇÕES
Um corpo de função pode conter chamada para outra função. A situação para esses tipos de chamadas de função é a mesma que se a chamada de função houvesse ocorrido na parte main do programa; a única restrição é que a declaração de função (ou definição de função) deve aparecer antes de a função ser usada. Se você houver montado seu programa conforme nossas orientações, isso ocorrerá automaticamente, já que todas as declarações de função vêm antes da parte main do programa e todas as definições de função vêm depois da parte main do programa. Embora seja possível incluir uma chamada de função dentro da definição de outra função, não se pode colocar a definição de uma função dentro do corpo de outra definição de função. FUNÇÃO ARREDONDADORA A tabela de funções predefinidas (Painel 3.2) não inclui qualquer função para o arredondamento de um número. As funções ceil e floor são quase, mas não completamente, funções arredondadoras. A função ceil sempre retorna o próximo número inteiro maior (ou seu argumento, se for um número inteiro). Assim, ceil(2.1) retorna 3.0, não 2.0. A função floor sempre retorna o próximo número inteiro menor que o argumento, ou igual a ele. Assim, floor(2.9) retorna 2.0, não 3.0. Felizmente, é fácil definir uma função que faça um verdadeiro arredondamento. A função é definida em Painel 3.6. A função round arredonda seu argumento para o inteiro mais próximo. Por exemplo, round(2.3) retorna 2 e round (2.6) retorna 3. Para verificar se round funciona corretamente, vamos utilizar alguns exemplos. Considere round(2.4) . O valor retornado é o seguinte (convertido em um valor int): floor(2.4 + 0.5)
que é floor(2.9) , ou 2.0. Na verdade, para qualquer número que seja maior ou igual a 2.0 e estritamente menor que 2.5, esse número mais 0.5 será menor que 3.0 e, assim, floor aplicado a esse número mais 0.5 retornará 2.0. Assim, round aplicado a qualquer número maior ou igual a 2.0 e estritamente menor que 2.5 apresentará como resultado 2. (Como a declaração de função para round especifica que o tipo para o valor retornado é int, convertemos o tipo do valor calculado em int.) Agora considere números maiores ou iguais a 2.5; por exemplo, 2.6. O valor retornado pela chamada round(2.6) é o seguinte (convertido em valor int): floor(2.6 + 0.5)
que é floor(3.1) ou 3.0. Na realidade, para qualquer número que seja maior que 2.5 e menor ou igual a 3.0, esse número mais 0.5 será maior que 3.0. Assim, round chamada com qualquer número que seja maior que 2.5 e menor que 3.0 apresentará como resultado 3. Desse modo, round funciona corretamente para todos os argumentos entre 2.0 e 3.0. É óbvio que não existe nada de especial em relação aos argumentos entre 2.0 e 3.0. Um argumento similar se aplica a todos os números não-negativos. Portanto, round funciona corretamente para todos os argumentos não-negativos.
Funções Definidas pelo Programador Painel 3.6
1 2 3
Função round #include #include using namespace std;
4 5 6
int round(double number); //Assumes number >= 0. //Returns number rounded to the nearest integer.
Teste do programa de função round
7 int main( ) 8 { 9 double doubleValue; char ans; 10 11 12 13 14 15 16 17 18 19
do { cout << "Forneça um valor double: "; cin >> doubleValue; cout << "Arredondado, esse número é " <> ans; }while (ans == ’s’ || ans == ’s’); cout << "Fim do teste.\n";
20 21 } 22 23 24 25 26
return 0;
//Uses cmath: int round(double number) { return static_cast(floor(number + 0.5)); }
DIÁLOGO PROGRAMA-USUÁRIO Forneça um valor double: Arredondado, esse número Outra vez? (s/n): s Forneça um valor double: Arredondado, esse número Outra vez? (s/n): n Fim do teste.
9.6 é 10 2.49 é 2
7. Qual é a saída produzida pelo seguinte programa? #include using namespace std; char misterio(int primeiroParametro, int segundoParametro); int main( ) { cout << misterio(10, 9) << "ato\n"; return 0; } char misterio(int primeiroParametro, int segundoParametro); {
<< endl;
73
74
Fundamentos das Funções
if (primeiroParametro
>= segundoParametro)
return ’G’’; else
8. 9.
10. 11.
■
return ’R’; } Escreva uma declaração de função (protótipo de função) e uma definição de função para uma função que necessite de três argumentos, todos de tipo int, e que forneça a soma desses três argumentos. Escreva uma declaração e uma definição de função para uma função que necessite de um argumento de tipo double. A função retorna o valor de caractere ’P’, se seu argumento for positivo, e ’N’, se seu argumento for zero ou negativo. Pode uma definição de função aparecer dentro do corpo de outra definição de função? Liste as similaridades e diferenças entre como se invoca (chama) uma função predefinida (ou seja, de biblioteca) e uma função definida pelo usuário.
FUNÇÕES QUE RETORNAM UM VALOR BOOLEANO
O tipo retornado por uma função pode ser bool. Uma chamada para uma função assim retorna um dos valores true ou false e pode ser usada em qualquer lugar onde uma expressão booleana seja permitida. Por exemplo, pode ser utilizada em uma expressão booleana para controlar um comando if-else ou um loop. Isso pode, muitas vezes, tornar um programa mais legível. Por meio de uma declaração de função, associa-se uma expressão booleana complexa a um nome com significado. Por exemplo, o comando if (((taxa
>= 10) && (taxa < 20)) || (taxa == 0))
{ ... } pode ser escrito if (apropriada(taxa)) { ... } desde que a seguinte função tenha sido definida: bool apropriada( int taxa) { return (((taxa >= 10) && (taxa < 20)) || (taxa == 0)); }
12. Escreva uma definição de função para uma função chamada emOrdem que requer três argumentos de tipo int. A função apresenta como saída true se os três argumentos estiverem em ordem ascendente; caso contrário, apresenta como saída false . Por exemplo, tanto emOrdem(1, 2, 3) quanto emOrdem(1, 2, 2) apresentam true como saída, enquanto emOrdem(1, 3, 2) apresenta false como saída. 13. Escreva uma definição de função para uma função chamada par que requer um argumento de tipo int e retorna um valor bool. A função apresenta true como saída se seu único argumento for um número par; caso contrário, apresenta false como saída. 14. Escreva uma definição de função para uma função chamada digito que requer um argumento de tipo char e retorna um valor bool. A função apresenta true como saída se o argumento for um dígito decimal; caso contrário, apresenta false como saída.
■
DEFININDO FUNÇÕES void
Em C++, uma função void é definida de forma similar à das funções que retornam um valor. Por exemplo, a função seguinte é uma função void que apresenta como saída o resultado de um cálculo que converte uma temperatura expressa em graus Fahrenheit para graus Celsius. O verdadeiro cálculo é feito em outra parte do programa. Esta função void implementa apenas a subtarefa de apresentar os resultados do cálculo.
Funções Definidas pelo Programador
75
void mostraResultados(double fGraus, double cGraus)
{
cout.setf(ios::fixed); cout.setf(ios::showpoint); cout.precision(1); cout << fGraus << " graus Fahrenheit equivalem a\n" << cGraus << " graus Celsius.\n";
}
Como a definição de função acima ilustra, há apenas duas diferenças entre uma definição de função para uma função void e outra para uma função que forneça um valor. Uma diferença é que nós utilizamos a palavra-chave void em lugar de especificarmos o tipo do valor a ser retornado. Isso diz ao compilador que essa função não retornará nenhum valor. O nome void (vazio, em inglês) é empregado como uma forma de dizer "nenhum valor é retornado por esta função". A segunda diferença é que uma definição de função void não requer um comando return. A execução da função termina quando o último comando no corpo da função é executado. Uma chamada de função void é um comando executável. Por exemplo, a função mostraResultados acima pode ser calculada da seguinte forma: mostraResultados(32.5, 0.3);
Se esse comando fosse executado em um programa, faria com que as seguintes linhas surgissem na tela: 32.5 graus Fahrenheit equivalem a 0.3 graus Celsius.
Observe que a chamada de função termina com um ponto-e-vírgula, que diz ao compilador que a chamada de função é um comando executável. Quando uma função void é chamada, os parâmetros formais são substituídos pelos argumentos, e os comandos no corpo da função são executados. Por exemplo, uma chamada à função void mostraResultados, que apresentamos anteriormente nesta seção, fará com que algumas linhas sejam escritas na tela. Uma forma de pensar em uma chamada a uma função void é imaginar que o corpo da função é copiado para dentro do programa no lugar da chamada de função. Quando a função é chamada, os parâmetros formais são substituídos pelos argumentos e, então, é exatamente como se o corpo da função fossem linhas do programa. (O Capítulo 4 descreve o processo de substituição de parâmetros formais por argumentos em detalhe. Até lá, utilizaremos apenas exemplos simples que sejam suficientemente claros sem uma descrição formal do processo de substituição.) É perfeitamente legal, e às vezes útil, haver uma função sem argumentos. Nesse caso, simplesmente não há parâmetros formais listados na declaração de função e nenhum argumento é utilizado quando a função é chamada. Por exemplo, a função void inicializaTela, definida a seguir, apenas imprime um comando de nova linha na tela: void inicializaTela(
)
{ cout << endl; }
Se seu programa inclui a seguinte chamada a essa função como seu primeiro comando executável, a saída do programa executado anteriormente será separada da saída do seu programa: inicializaTela( );
Observe que, mesmo quando não existem parâmetros em uma função, você ainda precisa incluir os parênteses na declaração de função e em uma chamada à função. A colocação da declaração de função (protótipo de função) e a definição de função é a mesma para funções void que a descrita para funções que retornam um valor. ■
COMANDOS return EM FUNÇÕES void
Tanto as funções void quanto as funções que retornam um valor podem ter comandos return. No caso de uma função que retorna um valor, o comando return especifica o valor retornado. No caso de uma função void,
76
Fundamentos das Funções
o comando return não inclui qualquer expressão para um valor retornado. Um comando return em uma função void apenas termina a chamada de função. Toda função que retorna um valor deve terminar executando um comando return. Entretanto, uma função void não precisa conter um comando return. Se não o contiver, terminará após executar o código no corpo da função. É como se houvesse um comando return implícito antes da chave de fechamento, }, ao final do corpo da função. DECLARAÇÃO DE FUNÇÃO (PROTÓTIPO DE FUNÇÃO) Uma declaração de função (protótipo de função) informa tudo o que você precisa saber para escrever uma chamada de função. Uma declaração de função (ou a definição de função completa) deve aparecer em seu código antes da chamada à função. Declarações de função normalmente são colocadas antes da parte main do seu programa.
SINTAXE Tipo_Retornado_Ou_void NomeDaFuncao(Lista_De_Parametros) ; em que Lista_De_Parametros é uma lista de parâmetros separada por vírgulas: Tipo_1 Parametro_Formal_1, Tipo_2 Parametro_Formal_2, ... ... Tipo_Final Parametro_Formal_Final
Não se esqueça deste ponto-e-vírgula.
EXEMPLOS double pesoTotal(int numero, double pesoDeUm);
//Retorna o peso total de numero itens //cujo peso unitário é pesoDeUm. void mostraResultados(double fGraus, double cGraus);
//Exibe uma mensagem dizendo que fGraus Fahrenheit //equivalem a cGraus Celsius.
O fato de que há um comando return implícito antes da chave de fechamento em um corpo de função não significa que você jamais necessite de um comando return em uma função void. Por exemplo, a definição de função no Painel 3.7 pode ser usada como parte de um programa de gerenciamento de restaurante. Essa função apresenta como saída instruções para dividir uma dada quantidade de sorvete entre as pessoas de uma mesa. Se não existirem pessoas na mesa (ou seja, se numero é igual a 0), o comando return dentro do comando if termina a chamada de função e evita uma divisão por zero. Se numero não é 0, a chamada de função termina quando o último comando cout é executado ao final do corpo da função. ■
PRÉ-CONDIÇÕES E PÓS-CONDIÇÕES
Uma boa forma de se escrever um comentário de declaração de função é dividi-lo em dois tipos de informação, chamadas pré-condição e pós-condição . A pré-condição afirma o que se presume ser verdade quando a função é chamada. A função não deve ser usada e não se deve esperar que atue corretamente a não ser que a pré-condição se sustente. A pós-condição descreve o efeito da chamada de função; ou seja, a pós-condição diz o que será verdadeiro depois que a função é executada em uma situação na qual a pré-condição se sustenta. Para uma função que retorna um valor, a pós-condição descreverá o valor retornado pela função. Para uma função que altera o valor
Painel 3.7 1 2
Uso de return em uma função void ( parte 1 de 2)
#include using namespace std;
3 void iceCreamDivision(int number, double totalWeight); 4 //Mostra na tela as instruções para dividir totalWeight onças de sorvete entre 5 //o número de pessoas. Se o número for 0, apenas uma mensagem de erro será mostrada. 6 7 8 9
int main( )
{ int number; double totalWeight;
Funções Definidas pelo Programador
Painel 3.7
Uso de
return em
uma função
void
( parte 2 de 2)
10 11 12 13
cout << "Informe o número de fregueses: "; cin >> number; cout << "Informe o peso do sorvete a dividir (em gramas): "; cin >> totalWeight;
14
iceCreamDivision(number, totalWeight);
15 16 }
return 0;
17 void iceCreamDivision(int number, 18 { 19 double portion; 20 21 22 23 24 25 26 27 28 }
if (number
77
double totalWeight)
== 0)
{ cout
<< "Não
é possível dividir entre zero fregueses.\n";
return ;
} portion = totalWeight/number; cout << "Cada um recebe " << portion << " gramas de sorvete." << endl;
Se o númerto for Ø, então a função de execução termina aqui.
DIÁLOGO PROGRAMA-USUÁRIO Informe o número de fregueses: 0 Informe o peso do sorvete a dividir (em gramas): 12 Não é possível dividir entre zero fregueses.
de algumas variáveis de argumento, a pós-condição descreverá todas as mudanças feitas nos valores dos argumentos. Por exemplo, eis uma declaração de função com pré-condição e pós-condição: void mostraJuro(double saldo, double taxa);
//Pré-condição: //taxa é a taxa //Pós-condição: //à determinada
saldo é um saldo não-negativo de uma conta de poupança. de juros expressa como porcentagem, como 5 para 5%. O valor em juros sobre um dado saldo taxa é mostrado na tela.
Você não precisa saber a definição da função mostraJuro a fim de utilizar essa função. Tudo o que você precisa saber é dado pela pré-condição e pós-condição. Quando a única pós-condição é uma descrição do valor retornado, os programadores geralmente omitem a palavra Pós-condição, como no seguinte exemplo: double celsius(double fahrenheit);
//Pré-condição: fahrenheit é uma temperatura em graus Fahrenheit. //Retorna a temperatura equivalente expressa em graus Celsius.
Alguns programadores preferem não usar as palavras pré-condição e pós-condição em seus comentários de funções. Entretanto, quer você utilize as palavras, quer não, deve sempre pensar em termos de pré-condição e póscondição quando projeta uma função e quando decide o que incluir no comentário da função. ■
main É UMA FUNÇÃO
Como já observamos, a parte main de um programa é, na realidade, a definição de uma função chamada main. Quando o programa é executado, a função main é automaticamente chamada; esta, por sua vez, pode chamar outras funções. Embora possa parecer que o comando return na parte main de um programa deveria ser opcional,
78
Fundamentos das Funções
na prática não é. O padrão C++ diz que você pode omitir o comando return 0 na parte main do seu programa, mas muitos compiladores ainda o exigem e quase todos eles permitem que você o inclua. Em nome da portabilidade, você deve incluir o comando return 0 na função main. Você deve considerar a parte main de um programa uma função que retorna um valor de tipo int e, assim, requer um comando return. Tratar a parte main do seu programa uma função que retorna um inteiro pode parecer estranho, mas é a tradição que muitos compiladores adotam. Embora alguns compiladores possam permitir que você o faça, não o aconselhamos a incluir uma chamada a main em seu código. Só o sistema deve chamar main, o que é feito quando seu programa é executado. ■
FUNÇÕES RECURSIVAS
O C++ permite que você defina funções recursivas. As funções recursivas serão abordadas no Capítulo 13. Se você não sabe o que são, não há necessidade de se preocupar até que chegue a esse capítulo. Se quiser saber a respeito de funções recursivas antes, leia as Seções 13.1 e 13.2 do Capítulo 13 depois de completar o Capítulo 4. Observe que a função main não deve ser chamada recursivamente.
15. Qual é a saída do seguinte programa? #include using namespace std; void amigavel( ); void timida(int contagemPlateia); int main( )
{
amigavel( ); timida(6); cout << "Mais uma vez:\n"; timida(2); amigavel( ); cout << "Fim do programa.\n"; return 0;
} void amigavel( )
{ cout << "Olá\n"; } void timida(int contagemPlateia)
{ if (contagemPlateia < 5) return ;
cout << "Adeus\n"; }
16. Suponha que você tenha omitido o comando return na definição de função para Divisao_Do_Sorvete no Painel 3.7. Que efeito isso exerceria sobre o programa? O programa compilaria ou não? Ele se comportaria de forma diferente? 17. Escreva uma definição para uma função void que possua três argumentos de tipo int e que apresente na tela o produto desses três argumentos. Coloque a definição em um programa completo que leia os três números e depois chame essa função. 18. Seu compilador permite void main( ) e int main ( )? Que mensagens de alerta são emitidas se você utilizar int main( ) e não retornar um comando return 0;? Para descobrir, escreva vários pequenos programas-teste ou pergunte ao seu orientador ou guru. 19. Inclua uma pré-condição e uma pós-condição para a função predefinida sqrt, que retorna a raiz quadrada de seu argumento.
Regras de Escopo
3.3
79
Regras de Escopo Que o final seja legítimo, que esteja dentro do escopo da Constituição... John Marshall, Presidente da Suprema Corte dos EUA, McCulloch v. Maryland (1803)
As funções devem ser unidades independentes, que não interferem com outras funções — ou com qualquer outro código. Para conseguir isso, muitas vezes é preciso fornecer à função variáveis próprias que são distintas de quaisquer outras variáveis declaradas fora da definição de função e que podem ter os mesmos nomes que as variá veis que pertençam à função. Essas variáveis que são declaradas em uma definição de função são chamadas variá- veis locais e são o assunto desta seção. ■
VARIÁVEIS LOCAIS
Dê uma olhada no programa do Painel 3.1. Ele inclui uma chamada à função predefinida sqrt. Não precisamos saber nada sobre os detalhes da definição de função de sqrt a fim de utilizar essa função. Em particular, não precisamos saber que variáveis foram declaradas na definição de sqrt. Uma função que você defina não é diferente. Declarações de variáveis dentro de uma definição de função são como se fossem declarações de variáveis em uma função predefinida ou em outro programa. Se você declara uma variável em uma definição de função e depois declara outra variável com o mesmo nome na função main do programa (ou no corpo de alguma outra definição de função), então essas duas variáveis são diferentes, mesmo que possuam o mesmo nome. Vamos ver um exemplo. O programa no Painel 3.8 possui duas variáveis chamadas ervilhaMedia; uma é declarada e utilizada na definição da função estimativaDoTotal, e a outra é declarada e utilizada na função main do programa. A variável ervilhaMedia na definição de função para estimativaDoTotal e a variável ervilhaMedia na função main são duas variáveis diferentes. É como se a função estimativaDoTotal fosse uma função predefinida. As duas variáveis chamadas ervilhaMedia não interferirão uma com a outra, tanto quanto duas variáveis em dois programas completamente diferentes não interfeririam. Painel 3.8 Variáveis locais ( parte 1 de 2) 1 2 3
//Calcula o rendimento médio de uma plantação experimental de ervilhas. #include using namespace std;
4 5 6 7 8
double estimateOfTotal(int minPeas, int maxPeas, int podCount);
//Retorna uma estimativa do número total de ervilhas colhidas. //O parâmetro formal podCount é o número de vagens. //Os parâmetros formais MinPeas são o número mínimo //e máximo de ervilhas em uma vagem.
9 int main( ) 10 { 11 int maxCount, minCount, podCount; 12 double averagePea , yield;
Esta variável chamada ervilhaMedia é o local da função main.
13 14 15 16 17 18
cout << "Informe o cin >> minCount >> cout << "Informe o cin >> podCount; cout << "Informe o cin >> averagePea;
número mínimo e máximo de ervilhas em uma vagem: "; maxCount; número de vagens: ";
19 20
yield = estimateOfTotal(minCount, maxCount, podCount) * averagePea;
21
cout.setf(ios::fixed);
peso de uma ervilha média (em onças): ";
80
Fundamentos das Funções
Painel 3.8
22 23 24 25 26 27 28 29 30
Variáveis locais ( parte 2 de 2)
cout.setf(ios::showpoint); cout.precision(3); cout << "Número mínimo de ervilhas por vagem = " << minCount << endl << "Número máximo de ervilhas por vagem = " << maxCount << endl << "Contagem de vagens = " << podCount << endl << "Peso médio da ervilha = " << averagePea << " onças" <
31 return 0; 32 } 33 34 double estimateOfTotal(int minPeas, 35 { 36 double averagePea; 37 38 39 }
int maxPeas, int podCount)
Esta variável chamada ervilhaMedia é o local da função estimativaDoTotal.
averagePea = (maxPeas + minPeas)/2.0; return (podCount * averagePea );
DIÁLOGO PROGRAMA-USUÁRIO Informe o número mínimo e máximo de ervilhas em uma vagem: 4 Informe o número de vagens: 10 Informe o peso de uma ervilha média (em onças*): 0.5 Número mínimo de ervilhas por vagem = 4 Número máximo de ervilhas por vagem = 6 Contagem de vagens = 10 Peso médio da ervilha = 0.500 onças Rendimento médio estimado = 25.000 onças
6
Quando a variável ervilhaMedia recebe um valor na chamada de função para estimativaDoTotal, isso não altera o valor da variável, também chamada ervilhaMedia, na função main. Uma variável declarada dentro do corpo de uma definição de função é chamada de local àquela função, ou então se diz que a função está em seu escopo. Se uma variável é local a alguma função, às vezes a chamamos simplesmente de variável local , sem especificar a função. Outro exemplo de variáveis locais pode ser visto no Painel 3.5. A definição da função custoTotal naquele programa começa assim: double custoTotal(int numeroParametro, double precoParametro)
{ const double IMPOSTO
= 0.05; //5% de imposto sobre vendas
double subtotal;
A variável subtotal é local à função custoTotal. A constante nomeada IMPOSTO também é local à função custoTotal. (Uma constante nomeada é, na verdade, nada mais do que uma variável que é inicializada com um valor e que não pode ter esse valor alterado.) VARIÁVEIS LOCAIS Uma variável definida dentro do corpo de uma função é chamada de local àquela função , ou então se diz que possui aquela função como escopo. Se a variável é local a uma função, então se pode ter outra variável (ou outro tipo de item) com o mesmo nome, declarada em outra definição de função; essas duas variáveis serão diferentes, embora possuam o mesmo nome. (Em particular, isso é verdade mesmo se uma das funções é a função main.)
*
Uma onça equivale a 28,35 g. (N. do R.T.)
Regras de Escopo ■
81
ABSTRAÇÃO PROCEDURAL
Uma pessoa que utiliza um programa não precisa conhecer os detalhes do código desse programa. Imagine como seria difícil sua vida se você tivesse de saber e lembrar o código para o compilador que você usa. Um programa tem um trabalho a fazer, como compilar seu programa ou verificar a ortografia das palavras em seu documento. Você precisa saber o que o programa faz, para poder utilizá-lo, mas não precisa (ou, pelo menos, não deveria precisar) saber como o programa realiza o trabalho. Uma função é como um programa pequeno e deve ser usada de forma similar. Um programador que utiliza uma função em um programa precisa saber o que a função faz (como calcular uma raiz quadrada ou converter uma temperatura em graus Fahrenheit para graus Celsius), mas não precisa saber como a função realiza essa tarefa. Geralmente se diz que isso é tratar uma função como se fosse uma caixa preta . Chamar algo de caixa preta é uma figura de linguagem que procura evocar a imagem de um dispositivo material que você sabe como usar, mas cujo método de operação é um mistério, pois está encerrado em uma caixa preta cujo conteúdo você não pode ver (e não pode abri-la). Se uma função é bem projetada, o programador pode utilizar a função como se fosse uma caixa preta. Tudo o que o programador precisa saber é que, se ele inserir os argumentos apropriados na caixa preta, ela efetuará uma ação apropriada. Projetar uma função para que possa ser utilizada como uma caixa preta às vezes é chamado de ocultação da informação, para enfatizar o fato de que o programador age como se o corpo da função estivesse oculto à sua visão. Escrever e utilizar funções como se fossem caixas pretas também é chamado de abstração procedural. Quando se programa em C++, talvez fizesse mais sentido chamar de abstração funcional . Entretanto, procedimento é um termo mais geral que função , e os cientistas da computação o utilizam para todos os sistemas de instruções "estilo função" e, assim, preferem o termo abstração procedural . O termo abstração transmite a idéia de que, quando se usa uma função, como uma caixa preta, abstraem-se os detalhes do código contido no corpo da função. Essa técnica pode ser chamada de princípio da caixa preta ou princípio da abstração procedural ou ocultação da informação . Os três termos querem dizer o mesmo. Seja lá como for chamado esse princípio, o importante é que você deve utilizá-lo quando projeta e escreve suas definições de função. ABSTRAÇÃO PROCEDURAL Quando aplicado a uma definição de função, o princípio da abstração procedural significa que sua função deve ser escrita de modo que possa ser utilizada como uma caixa preta. Isso quer dizer que o programador que utiliza a função não deve precisar olhar para o corpo da definição da função para ver como a função opera. A declaração de função e o comentário que a acompanha são tudo o que o programador precisa saber a fim de utilizar a função. Para garantir que suas definições de função tenham essa importante propriedade, obedeça estritamente às seguintes regras:
COMO ESCREVER UMA DEFINIÇÃO DE FUNÇÃO CAIXA PRETA ■ ■
■
O comentário da declaração de função deve dizer ao programador toda e qualquer condição requerida dos argumentos da função e deve descrever o resultado de uma invocação à função. Todas as variáveis utilizadas no corpo da função devem ser declaradas no corpo da função. (Os parâmetros formais não precisam ser declarados, porque estão listados no cabeçalho da função.)
CONSTANTES GLOBAIS E VARIÁVEIS GLOBAIS
Como observamos no Capítulo 1, você pode e deve dar nome a valores constantes utilizando o modificador const. Por exemplo, no Painel 3.5 usamos o modificador const para dar nome à taxa de imposto sobre vendas com a seguinte declaração: const double IMPOSTO
= 0.05; // 5% de imposto sobre vendas
Se essa declaração estiver dentro da definição de uma função, como no Painel 3.5, o nome IMPOSTO é local à definição de função, o que significa que, fora da definição da função que contém a declaração, você pode usar o nome IMPOSTO para outra constante nomeada, ou variável, ou qualquer outra coisa. Por outro lado, se essa declaração aparecer no início do programa, fora do corpo de todas as funções (e fora do corpo da parte main do programa), diz-se que a constante nomeada é uma constante nomeada global e a constante nomeada pode ser usada em qualquer definição de função que siga a declaração da constante.
82
Fundamentos das Funções
O Painel 3.9 mostra um programa com um exemplo de uma constante nomeada global. O programa pede o valor de um raio e depois calcula tanto a área de um círculo quanto o volume de uma esfera com aquele raio, utilizando as seguintes fórmulas: 2 area = π x (raio) 3 volume = (4/3) x π x (raio) Ambas as fórmulas incluem a constante π, que é aproximadamente igual a 3.14159. O símbolo grega chamada "pi". O programa utiliza a seguinte constante nomeada global: const double PI
é a letra
π
= 3.14159;
que aparece fora da definição de qualquer função (inclusive fora da definição de main). O compilador permite a você uma ampla liberdade quanto ao local onde deve colocar as declarações de suas constantes nomeadas globais. Para facilitar a leitura, contudo, você deve colocar todas as suas instruções de include juntas, todas as suas declarações de constantes nomeadas globais em outro grupo e todas as suas declarações de função (protótipos de função) juntas. Seguiremos a prática-padrão e colocaremos todas as nossas declarações de constantes nomeadas globais após as instruções de include e using e antes das declarações de função. Painel 3.9
1 2 3 4 5
Constante nomeada global ( parte 1 de 2) //Calcula a área de um círculo e o volume de uma esfera. //Utiliza o mesmo raio para ambos os cálculos. #include #include using namespace std;
6
const double PI
= 3.14159;
7 double area(double radius); 8 //Retorna a área de um círculo com o raio especificado. 9 double volume(double radius); 10 //Retorna o volume de uma esfera com o raio especificado. 11 int main( ) 12 { 13 double radiusOfBoth, areaOfCircle, volumeOfSphere; 14 15 16
cout << "Informe um raio que será utilizado tanto em um círculo" << "quanto em uma esfera (em polegadas): "; cin >> radiusOfBoth;
17 18
areaOfCircle = area(radiusOfBoth); volumeOfSphere = volume(radiusOfBoth);
19 20 21 22 23
cout << << << << <<
"Raio = " << radiusOfBoth << " polegadas\n" "Área do círculo = " << areaOfCircle " polegadas quadradas\n" "Volume da esfera = " << volumeOfSphere " polegadas cúbicas\n";
24 return 0; 25 } 26 27 double area(double radius) 28 { 29 return (PI * pow(radius, 2)); 30 } 31 double volume(double radius) 32 { 33 return ((4.0/3.0) * PI * pow(radius, 3)); 34 }
Regras de Escopo Painel 3.9
83
Constante nomeada global ( parte 2 de 2)
DIÁLOGO PROGRAMA-USUÁRIO Informe um raio que será utilizado tanto em um círculo quanto em uma esfera (em polegadas): 2 Raio = 2 polegadas* Área do círculo = 12.5664 polegadas quadradas** Volume da esfera = 31.5103 polegadas cúbicas***
Colocar todas as constantes nomeadas no início do programa aumenta a legibilidade, mesmo se a constante nomeada for usada apenas por uma função. Se for preciso alterar a constante nomeada em uma futura versão do programa, será mais fácil encontrá-la se estiver no início. Por exemplo, colocar a declaração da constante para a taxa de imposto sobre vendas no início de um programa de contabilidade tornará mais fácil revisar o programa se a taxa do imposto mudar. É possível declarar variáveis ordinárias, sem o modificador const, como variáveis globais, que são acessíveis a todas as definições de função no arquivo. Isso é feito de maneira similar à utilizada para as constantes nomeadas globais, a não ser pelo fato de o modificador const não ser usado na declaração de variável. Entretanto, raramente há necessidade de se utilizar tais variáveis globais. Além disso, as variáveis globais podem tornar um programa mais difícil de se entender e manter, por isso o aconselhamos a evitá-las.
20. Se você utilizar uma variável em uma definição de função, onde deve declarar a variável? Na definição de função? Na função main? Em qualquer lugar que seja conveniente? 21. Suponha que uma função chamada funcao1 possua uma variável chamada sam declarada dentro da definição de funcao1 e uma função chamada funcao2 que também possui uma variável chamada sam declarada dentro da definição de funcao2. Será que o programa compilará (presumindo que todo o resto esteja correto)? Se for executado, gerará uma mensagem de erro ao ser executado (presumindo que todo o resto esteja correto)? Se for executado e não produzir mensagem de erro na execução, fornecerá a resposta correta (presumindo que todo o resto esteja correto)? 22. Qual é a finalidade do comentário que acompanha a declaração de uma função? 23. Qual é o princípio da abstração procedural como aplicado a definições de função? 24. O que significa quando dizemos que o programador que utiliza uma função poderia tratar a função como uma caixa preta? (Esta pergunta está intimamente relacionada à pergunta anterior.)
■ BLOCOS
Uma variável declarada dentro de um comando composto (ou seja, dentro de chaves) é local ao comando composto. O nome da variável pode ser usado para algo mais, como o nome de uma variável diferente, fora do comando composto. Um comando composto com declarações normalmente é chamado de bloco. Na realidade, bloco e comando composto são dois termos que designam a mesma coisa. Entretanto, quando nos concentramos nas variáveis declaradas dentro de um comando composto, normalmente utilizamos o termo bloco em vez de comando composto e dizemos que as variáveis declaradas dentro do bloco são locais ao bloco . Se uma variável é declarada em um bloco, a definição se aplica desde o local da declaração até o final do bloco. Costuma-se dizer que o escopo da declaração vai desde o local da declaração até o final do bloco. Assim, se uma variável é declarada no início de um bloco, a declaração não surte efeito até que o programa chegue ao local da declaração (veja Exercício de Autoteste 25). Observe que o corpo de uma definição de função é um bloco. Assim, uma variável que é local a uma função é a mesma coisa que uma variável que é local ao corpo da definição de função (que é um bloco). * Uma polegada equivale a 2,54 cm. (N. do R.T.) ** Uma polegada quadrada equivale a 6,452 cm 2. (N. do R.T.) *** Uma polegada cúbica equivale a 16,39 cm3. (N. do R.T.)
84
Fundamentos das Funções
BLOCOS Um bloco é um código em C++ entre chaves. As variáveis declaradas em um bloco, são locais ao bloco, e, portanto, os nomes das variáveis podem ser usados fora do bloco para alguma outra coisa (como ser reutilizadas como nomes para variáveis diferentes).
ESCOPOS ANINHADOS Suponha que você tenha um bloco aninhado dentro de outro bloco e que um identificador é declarado como uma variável em cada um desses blocos. Existem duas variáveis diferentes com o mesmo nome. Uma variável existe só dentro do bloco interno e não se pode ter acesso a ela fora do bloco interno. A outra variável existe apenas no bloco externo e não se pode ter acesso a ela no bloco interno. As duas variáveis são distintas, portanto mudanças realizadas em uma delas não exercerão efeito sobre a outra. ■
REGRA DE ESCOPO PARA BLOCOS ANINHADOS Se um identificador é declarado como uma variável em cada um de dois blocos, um dentro do outro, então temos duas variáveis diferentes com o mesmo nome. Uma variável existe apenas dentro do bloco interno e não se pode ter acesso a ela de fora do bloco interno. A outra variável existe apenas no bloco externo e não se pode ter acesso a ela do bloco interno. As duas variá veis são distintas, portanto mudanças realizadas em uma delas não exercerão efeito sobre a outra.
USE CHAMADAS
DE FUNÇÕES EM COMANDOS DE SELEÇÃO E LOOPS O comando switch e o comando if-else permitem a introdução de vários comandos diferentes em cada seleção. Entretanto, isso pode tornar o comando switch ou if-else difíceis de ler. Em vez de colocar um comando composto em uma estrutura de controle, normalmente é preferível converter o comando composto em uma definição de função e colocar uma chamada de função na ramificação. De maneira similar, se o corpo de um loop é extenso, é preferível converter o comando composto em uma definição de função e transformar o corpo do loop em uma chamada de função.
VARIÁVEIS DECLARADAS EM UM LOOP for Uma variável pode ser declarada no cabeçalho de um comando for de modo que a variável seja ao mesmo tempo declarada e inicializada no início do comando for. Por exemplo, ■
for (int n
= 1; n <= 10; n++) soma = soma + n;
O padrão C++ ANSI/ISO requer que um compilador C++ que alegue obedecer ao padrão trate qualquer declaração na inicialização de um loop for como se fosse local ao corpo do loop. Os compiladores C++ antigos não fazem isso. Você deve verificar como seu compilador trata as variáveis declaradas em uma inicialização de loop for. Se a portabilidade for muito importante para sua aplicação, você não deve escrever código que dependa desse comportamento. Com o tempo, todos os compiladores C++ amplamente usados se adaptarão a essa regra, mas os compiladores disponíveis atualmente podem ou não obedecer a ela.
25. Embora não o encorajemos a programar utilizando este estilo, estamos incluindo um exercício que utiliza blocos aninhados para ajudá-lo a entender as regras de escopo. Determine a saída que este fragmento de código produziria se inserido em um programa completo que, a não ser por este trecho, seria correto. { int x
= 1;
cout << x << endl; { cout << x << endl; int x
= 2;
Regras de Escopo
85
cout << x << endl; { cout << x << endl; int x = 3; cout << x << endl; } cout << x << endl; } cout << x << endl; }
■ ■
■
■ ■
Existem dois tipos de funções em C++: funções que retornam um valor e funções void. Uma função deve ser definida de forma que possa ser utilizada como uma caixa preta. O programador que utiliza a função não precisa saber os detalhes do código dessa função. Tudo o que o programador precisa saber é a declaração da função e os comentários que a acompanham, que descrevem o valor retornado. Essa regra, às vezes, é chamada de princípio da abstração procedural. Uma boa forma de escrever um comentário de declaração de função é utilizar uma pré-condição e uma pós-condição. A pré-condição afirma o que se presume que seja verdade quando a função é chamada. A póscondição descreve o efeito da chamada de função; ou seja, a pós-condição diz o que será verdade depois que a função for executada em uma situação em que a pré-condição se sustente. Uma variável declarada em uma definição de função é chamada de local à função. Um parâmetro formal é um tipo de "guardador" de lugar que é preenchido com um argumento de função quando a função é chamada. Os detalhes desse processo de "preenchimento" serão abordados no Capítulo 4.
RESPOSTAS DOS EXERCÍCIOS DE AUTOTESTE 1. 4.0 4.0 8.0 8.0 8.0 1.21 3 3 0 3.0 3.5 3.5 6.0 6.0 5.0 5.0 4.5 4.5 3 3.0 3.0 2. a. sqrt(x + y) b. pow(x, y + 7) c. sqrt(area + caramelo) d. sqrt(tempo+mare)/ninguem e. (-b + sqrt(b*b - 4*a*c))/(2*a) f. abs(x - y) or labs(x - y) or fabs(x - y) 3. #include #include using namespace std; int main( ) { int i; for (i = 1; i <= 10; i++) cout << "A raiz quadrada de " << i << " é " << sqrt(i) << endl; return 0;
86
Fundamentos das Funções
}
4. O argumento é fornecido ao sistema operacional. No que se refere ao seu programa em C++, você pode utilizar qualquer valor int como argumento. Por convenção, todavia, usa-se 1 para uma chamada a exit provocada por um erro, e 0 nos outros casos. 5. (5 + (rand( ) % 6)) 6. #include #include using namespace std; int main( ) { cout << "Forneça um inteiro não-negativo para usar como\n" << "semente para o gerador de números aleatórios: "; unsigned int semente; cin >> semente; srand(semente);
cout << "Aqui há dez possibilidades aleatórias:\n"; int i; for (i = 0; i < 10; i++) cout << ((RAND_MAX - rand( ))/static_cast(RAND_MAX)) << endl; return 0;
}
7. Wow 8. A declaração de função é int soma(int n1, int n2, int n3); //Retorna a soma de n1, n2, e n3.
A definição de função é int soma(int n1, int n2, int n3) { return (n1 + n2 + n3); }
9. A declaração de função é char testePositivo(double numero)
//Retorna ’P’ se número é positivo. //Retorna ’N’ se número é negativo ou zero. A definição de função é char testePositivo(double numero) { if (numero > 0) return ’P’; else return ’N’; }
10. Não, uma definição de função não pode aparecer dentro do corpo de outra definição de função. 11. Funções predefinidas e funções definidas pelo usuário são invocadas (chamadas) da mesma forma. 12. bool emOrdem(int n1, int n2, int n3) { return ((n1 <= n2) && (n2 <= n3)); } 13. bool par(int n) { return ((n % 2) == 0); }
Respostas dos Exercícios de Autoteste
87
14. bool digito(char ch) { return (’0’ <= ch) && (ch <= ’9’);
} 15. Olá Adeus Mais uma vez: Olá Final do programa.
16. Se você omitiu o comando return na definição de função para DivisaoDoSorvete no Painel 3.7, o programa compilará e será executado. Entretanto, se você inserir zero como entrada para o número de fregueses, o programa sofrerá um erro de execução devido a uma divisão por zero. 17. #include using namespace std; void produto(int n1, int n2, int n3); int main( )
{ int num1, num2, num3;
cout << "Forneça três números inteiros: "; cin >> num1 >> num2 >> num3; produto(num1, num2, num3); return 0; } void produto(int n1, int n2, int n3)
{ cout << "O produto dos três números " << n1 << ", " << n2 << " e" << n3 << " é " << (n1*n2*n3) << endl; }
18. Essas respostas dependem do sistema. 19. double sqrt(double n); //Pré-condição: n >= 0. //Retorna a raiz quadrada de n. Você pode reescrever a segunda linha de comentário da seguinte forma, se preferir, mas a versão acima é a forma usual utilizada para uma função que retorna um valor: //Pós-condição: Retorna a raiz quadrada de n. 20. Se você usar uma variável em uma definição de função, deve declarar a variável no corpo da definição de função. 21. Tudo vai dar certo. O programa compilará (presumindo-se que todo o resto esteja correto). O programa será executado (presumindo-se que todo o resto esteja correto). O programa não gerará mensagem de erro quando for executado (presumindo-se que todo o resto esteja correto). O programa fornecerá a saída correta (presumindo-se que todo o resto esteja correto). 22. O comentário explica que ação a função efetua, inclusive o valor retornado, e apresenta qualquer outra informação de que você necessite a fim de utilizar a função. 23. O princípio da abstração procedural afirma que uma função deve ser escrita de modo a poder ser utilizada como uma caixa preta. Isso significa que o programador que utiliza a função não precisa olhar para o corpo da definição de função para saber como essa função atua. A declaração de função e os comentários que a acompanham devem ser suficientes para que o programador possa utilizar a função. 24. Quando dizemos que o programador que utiliza a função deve ser capaz de tratar a função como uma caixa preta, queremos dizer que o programador não precisa olhar para o corpo da definição de função para saber como a função atua. A declaração de função e os comentários que a acompanham devem ser suficientes para que o programador possa utilizar a função.
88
Fundamentos das Funções
25. Alterar levemente o código ajuda a entender a que se refere cada declaração. O código possui três variáveis diferentes chamadas x. No trecho seguinte, renomeamos essas três variáveis como x1, x2 e x3. A saída é dada nos comentários. { int x1
= 1;// saída nesta coluna cout << x1 << endl;// 1 { cout << x1 << endl;// 1 int x2 = 2; cout << x2 << endl;// 2 { cout << x2 << endl;// 2 int x3 = 3; cout << x3 << endl;// 3 } cout << x2 << endl;// 2 } cout << x1 << endl;// 1
}
PROJETOS DE PROGRAMAÇÃO 1. Um litro equivale a 0.264179 galões. Escreva um programa que leia o número de litros de gasolina consumidos pelo carro do usuário e o número de milhas * que o carro andou e apresente como saída o número de milhas por galão que o carro rendeu. Seu programa deve permitir que o usuário repita o cálculo quantas vezes quiser. Defina uma função para calcular o número de milhas por galão. Seu programa deve usar uma constante globalmente definida para o número de galões por litro. 2. Escreva um programa para medir a taxa de inflação no ano passado. O programa pede o preço de um item (como um cachorro quente ou um diamante de um quilate) no ano passado e hoje. Estima a taxa de inflação como a diferença no preço dividida pelo preço do ano passado. Seu programa deve permitir que o usuário repita esse cálculo quantas vezes desejar. Defina uma função para calcular a taxa de inflação. A taxa de inflação deve ser um valor de tipo double, fornecendo a taxa como porcentagem, por exemplo, 5.3 para 5.3%. 3. Aperfeiçoe o programa do exercício anterior fazendo com que apresente também o preço estimado do item um e dois anos depois da época do cálculo. O aumento no custo em um ano é estimado como a taxa de inflação multiplicada pelo preço no início do ano. Defina uma segunda função para determinar o custo estimado de um item em um número especificado de anos, dados o preço atual do item e a taxa de inflação como argumentos. 4. A força de atração gravitacional entre dois corpos com massas m 1 e m 2 , separados por uma distância d , é dada pela seguinte fórmula: F
=
Gm 1m 2 d 2
onde G é a constante de gravitação universal: G =
6.673 x 10-8 cm3/(g • sec2)
Escreva uma definição de função que utilize argumentos para as massas de dois corpos e a distância entre eles e forneça a força gravitacional entre eles. Como você irá utilizar a fórmula acima, a força gravitacional será dada em dynes (dinas). Um dyne (dina) equivale a 1g • cm/sec2 Você deve usar uma constante globalmente definida para a constante de gravitação universal. Insira sua definição de função em um programa completo que calcule a força gravitacional entre dois objetos com dados de entrada adequados. Seu programa deve permitir que o usuário repita esse cálculo quantas vezes desejar. *
Uma milha terrestre equivale a 1,609 km. (N. do R.T.)
Projetos de Programação
89
5. Escreva um programa que peça a altura, o peso e a idade do usuário e calcule o tamanho das roupas de acordo com as seguintes fórmulas. * ■ Tamanho do chapéu = peso em libras dividido pela altura em polegadas e tudo isso multiplicado por 2.9. ■ Tamanho do casaco (tórax em polegadas) = altura vezes peso dividido por 288 e um ajuste efetuado pelo acréscimo de um oitavo de uma polegada para cada 10 anos acima dos 30 anos. (Observe que o ajuste só ocorre após 10 anos completos. Assim, não há ajuste para as idades de 30 a 39, mas um oitavo de uma polegada é acrescentado para a idade 40.) ■ Cintura em polegadas = peso dividido por 5.7 e um ajuste efetuado pelo acréscimo de um décimo de uma polegada para cada 2 anos acima dos 28 anos. (Observe que o ajuste só ocorre após 2 anos completos. Assim, não há ajuste para os 29 anos, mas um décimo de uma polegada é acrescentado para os 30 anos.) Utilize funções para cada cálculo. Seu programa deve permitir que o usuário repita esse cálculo quantas vezes desejar. 6. Escreva uma função que calcule o desvio médio e padrão de quatro pontuações. O desvio-padrão é definido como a raiz quadrada da média dos quatro valores: (s i - a) 2 , em que a é a média das quatro pontuações, s 1, s 2 , s 3 e s 4. A função terá seis parâmetros e chamará duas outras funções. Insira a função em um programa que lhe permita testar a função repetidas vezes até dizer ao programa que terminou. 7. Quando está frio, os meteorologistas transmitem um índice chamado fator de frio do vento , que leva em consideração a velocidade do vento e a temperatura. O índice fornece uma medida do efeito resfriador do vento em uma dada temperatura do ar. Esse índice pode ser aproximado pela seguinte fórmula: W = 13.12 + 0.6215*t – 11.37*v 0.16 + 0.3965*t*v 0.016 em que v = velocidade do vento em m/s t = temperatura em graus Celsius: t <= 10 W = índice de frio do vento (em graus Celsius) Escreva uma função que forneça o índice de frio do vento. Seu código deve assegurar que a restrição a respeito da temperatura não seja violada. Verifique alguns boletins meteorológicos em edições anteriores de jornais em sua biblioteca e compare o índice de frio do vento que você calculou com o resultado di vulgado no jornal.
*
Uma libra equivale a 453,6 g. (N. do R.T.)
Parâmetros e Sobrecarga Parâmetros e Sobrecarga
Capítulo 4Parâmetros e Sobrecarga
É só preencher os espaços em branco. Instrução comum
INTRODUÇÃO Este capítulo discute os detalhes dos mecanismos utilizados pelo C++ para conectar argumentos a parâmetros em chamadas de função. Discute também a sobrecarga, que é uma forma de dar duas (ou mais) definições de função diferentes para o mesmo nome de função. Finalmente, trata de algumas técnicas básicas para testar funções.
4.1
Parâmetros Não se pode colocar um pino quadrado em um buraco redondo. Ditado popular
Esta seção descreve os detalhes dos mecanismos utilizados pelo C++ para conectar um argumento a um parâmetro formal quando uma função é invocada. Existem dois tipos básicos de parâmetros e, portanto, dois mecanismos básicos de conexão em C++. Os dois tipos básicos de parâmetros são parâmetros chamados por valor e parâmetros chamados por referência . Todos os parâmetros que aparecem antes deste ponto no livro eram parâmetros chamados por valor. Com parâmetros chamados por valor , apenas o valor do argumento é conectado. Com os parâmetros chamados por referência , o argumento é uma variável e a própria variável é conectada; portanto, o valor das variáveis pode ser alterado pela invocação da função. Um parâmetro chamado por referência é indicado pela anexação do sinal de “e” comercial, &, ao tipo do parâmetro, como ilustrado pelas seguintes declarações de função: void
getEntrada(double& variavelUm,
int&
variavelDois);
Um parâmetro chamado por valor é indicado pela ausência do “e” comercial. Os detalhes sobre os parâmetros chamados por valor e por referência serão dados nas próximas subseções.
PARÂMETROS CHAMADOS POR VALOR Os parâmetros chamados por valor são mais do que apenas espaços em branco preenchidos com os valores dos argumentos da função. Um parâmetro chamado por valor é, na realidade, uma variável local. Quando a função é invocada, o valor de um parâmetro chamado por valor é calculado, e o parâmetro chamado por valor correspondente, que é uma variável local, é inicializado com esse valor. Na maioria dos casos, pode-se pensar em um parâmetro chamado por valor como um tipo de espaço em branco, ou “guardador” de lugar, que é preenchido pelo valor do argumento correspondente na invocação da função. Entretanto, em alguns casos é útil empre■
92
Parâmetros e Sobrecarga
gar um parâmetro chamado por valor como uma variável local e alterar o valor de seu parâmetro dentro do corpo da definição de função. Por exemplo, o programa no Painel 4.1 ilustra um parâmetro chamado por valor utilizado como uma variável local cujo valor é alterado no corpo da definição de função. Observe o parâmetro formal minutosTrabalhados na definição da função taxa. Ele é usado como uma variável, e seu valor é alterado pela seguinte linha, que aparece dentro da definição de função: minutosTrabalhados = horasTrabalhadas*60 + minutosTrabalhados; Painel 4.1
Parâmetro formal utilizado como variável local
1 2 3
//Programa de faturamento de um escritório de advocacia. #include using namespace std;
4
const double RATE = 150.00; //Dólares por 15 minutos de consulta.
5 6 7
double fee(int hoursWorked, int minutesWorked);
//Retorna o preço da hora hoursWorked e //os minutos minutesWorked de serviços.
8 int main( ) 9 { 10 int hours, minutes; 11 double bill; 12 13 14 15 16 17
cout << “Bem-vindo ao escritório de advocacia de \n” << “Dewey, Cheatham e Howe.\n” << “O escritório que tem coração.n” Os valores dos minutos não são << “Informe as horas e minutos” alterados para a chamada honorários. << “ de sua consulta:\n”; cin >> hours >> minutes;
18
bill = fee(hours, minutes );
19 20 21 22 23
cout.setf(ios::fixed); cout.setf(ios::showpoint); cout.precision(2); cout << “Por ” << hours << “ horas e ” << minutes << “ minutos, sua conta é de $” << bill << endl;
24 return 0; 25 } 26 double fee(int hoursWorked, int minutesWorked ) 27 { 28 int quarterHours; 29 30 31 32 }
minutesWorked = hoursWorked*60 + minutesWorked; quarterHours = minutesWorked/15; return (quarterHours*RATE);
DIÁLOGO PROGRAMA-USUÁRIO Bem-vindo ao escritório de advocacia de Dewey, Cheatham e Howe. O escritório que tem coração. Informe as horas e minutos de sua consulta: 5 46
Por 5 horas e 46 minutos, sua conta é de $3450.00
MinutosTrabalhados é
uma local variável inicializada para os valores dos minutos.
Parâmetros
93
Parâmetros chamados por valor são variáveis locais exatamente como as variáveis declaradas no corpo de uma função. Entretanto, não se deve acrescentar uma declaração de variável para os parâmetros formais. Listar o parâmetro formal minutosTrabalhados no cabeçalho da função serve também como a declaração da variável. A forma seguinte é errada e não deve ser utilizada para iniciar a definição de função para taxa porque declara minutosTrabalhados duas vezes: double
fee(int hoursWorked,
int
minutesWorked)
{ int quarterHours; int minutesWorked;
. . .
Não faça isso quando minutosTrabalhados for um parâmetro!
1. Descreva cuidadosamente o mecanismo do parâmetro chamado por valor. 2. Supõe-se que a seguinte função exija como argumentos um comprimento expresso em pés e polegadas e retorne o número total de polegadas. Por exemplo, totalPolegadas(1, 2) deve apresentar como saída 14, porque 1 pé e 2 polegadas é o mesmo que 14 polegadas. A função seguinte atuará corretamente? Se não, por quê? double totalPolegadas(int pes, int polegadas) { polegadas = 12*pes + polegadas; return polegadas; }
■
PRIMEIRA VISÃO DOS PARÂMETROS CHAMADOS POR REFERÊNCIA
O mecanismo de chamada por valor que utilizamos até agora não é suficiente para todas as tarefas que se possa querer que uma função desempenhe. Por exemplo, uma tarefa comum para uma função é obter um valor de entrada do usuário e estabelecer o valor de uma variável de argumento para esse valor de entrada. Com os parâmetros formais chamados por valor que utilizamos até agora, um argumento correspondente em uma chamada de função pode ser uma variável, mas a função recebe apenas o valor da variável e não altera a variável de forma alguma. Com um parâmetro formal chamado por valor apenas o valor do argumento substitui o parâmetro formal. Para uma função de entrada, o que se quer é que a variável (não o valor da variável) substitua o parâmetro formal. O mecanismo de chamada por referência atua exatamente dessa forma. Com um parâmetro formal chamado por referência, o argumento correspondente em uma chamada de função deve ser uma variável, e essa variável de argumento substitui o parâmetro formal. É quase como se a variável do argumento fosse literalmente copiada para dentro do corpo da definição de função em lugar do parâmetro formal. Depois que a substituição é efetuada, o código no corpo da função é executado e pode alterar o valor da variável do argumento. Um parâmetro chamado por referência deve ser assinalado de alguma forma para que o compilador o distinga de um parâmetro chamado por valor. O modo como se indica um parâmetro chamado por referência é a anexação de um sinal de “e” comercial, &, ao final do nome do tipo na lista de parâmetros formais. Isso é feito tanto na declaração da função (protótipo da função) como no cabeçalho da definição de função. Por exemplo, a seguinte definição de função possui um parâmetro formal, receptor, que é um parâmetro chamado por referência: void
getEntrada(double& receptor)
{ cout << “Forneça o número de entrada:\n”; cin >> receptor; }
Em um programa que contenha essa definição de função, a seguinte chamada de função fixará a variável ble numeroEntrada como igual a um valor lido a partir do teclado:
dou-
getEntrada(numeroEntrada);
O C++ permite que você coloque o símbolo de “e” comercial junto ao nome do tipo ou junto ao nome do parâmetro. Assim, às vezes você verá
94
Parâmetros e Sobrecarga
void getEntrada(double &receptor);
que equivale a void getEntrada(double& receptor); Painel 4.2
Parâmetros chamados por referência
1 2 3
//Programa para demonstrar parâmetros chamados por referência. #include using namespace std;
4 5
void getNumbers(int& input1, int& input2 );
6 7
void swapValues(int& variable1, int& variable2 );
8 9
void showResults(int output1, int output2);
//Lê dois inteiros a partir do teclado.
//Troca os valores variable1 e variable2.
//Mostra os valores de variable1 e variable2, nessa ordem.
10 int main( ) 11 { int firstNum, secondNum; 12 13 14 15 16 17 }
getNumbers(firstNum, secondNum); swapValues(firstNum, secondNum); showResults(firstNum, secondNum); return 0;
18 void getNumbers( int& input1, int& input2 ) 19 { 20 cout << “Forneça dois números inteiros: ”; 21 cin >> input1 22 >> input2; 23 } 24 void swapValues(int& variable1, int& variable2) 25 { 26 int temp; 27 28 29 30 31 32 33 34 35 36
temp = variable1; variable1 = variable2; variable2 = temp; } void showResults(int output1, int output2)
{ cout << “Em ordem inversa, os números são: ” << output1 << “ ” << output2 << endl; }
DIÁLOGO PROGRAMA-USUÁRIO Forneça dois números inteiros: 5 6 Em ordem inversa, os números são: 6 5
O Painel 4.2 apresenta os parâmetros chamados por referência. O programa lê dois números e escreve esses números na tela, mas em ordem inversa. Os parâmetros nas funções getNumeros e trocaValores são parâmetros chamados por referência. A entrada é efetuada pela chamada de função
Parâmetros
95
getNumeros(primeiroNum, segundoNum);
Os valores das variáveis primeiroNum e segundoNum são estabelecidos por essa chamada de função. Depois disso, a função seguinte inverte os valores nas duas variáveis primeiroNum e segundoNum: swapNumeros(primeiroNum, segundoNum);
As próximas subseções descrevem o mecanismo de chamada por referência em mais detalhes e explicam também as funções particulares utilizadas no Painel 4.2. PARÂMETROS CHAMADOS POR REFERÊNCIA Para transformar um parâmetro formal em parâmetro chamado por referência, anexe o símbolo de “e” comercial, &, ao seu nome de tipo. O argumento correspondente em uma chamada à função deve, então, ser uma variável, não uma constante ou outra expressão. Quando a função é chamada, o argumento da variável correspondente (não seu valor) substituirá o parâmetro formal. Qualquer alteração no parâmetro formal no corpo da função será feita na variável do argumento quando a função é chamada. Os detalhes exatos dos mecanismos de substituição serão fornecidos no texto deste capítulo.
EXEMPLO void getDados(int&
primeiraEntrada,
double&
segundaEntrada);
MECANISMO DE CHAMADA POR REFERÊNCIA EM DETALHE Na maioria das situações o mecanismo de chamada por referência funciona como se o nome da variável dado como argumento da função substituísse literalmente o parâmetro formal chamado por referência. Entretanto, o processo é um pouco mais sutil do que isso. Em algumas situações, essa sutileza é importante. Por isso, precisamos analisar mais detalhadamente esse processo de substituição de chamada por referência. Variáveis em um programa são implementadas como posições na memória. Cada posição na memória possui um endereço único, que é um número. O compilador atribui uma posição de memória a cada variável. Por exemplo, quando o programa no Painel 4.2 é compilado, pode ser atribuída à variável primeiroNum a posição 1010, e à variável segundoNum a posição 1012. Para todos os efeitos práticos, essas posições de memória são as variáveis. Por exemplo, considere a seguinte declaração de função do Painel 4.2: ■
void getNumeros(int&
entrada1,
int&
entrada2);
Os parâmetros formais chamados por referência entrada1 e entrada2 são “guardadores” de lugar para os verdadeiros argumentos utilizados em uma chamada de função. Agora considere uma chamada de função como a seguinte, do mesmo programa: getNumeros(primeiroNum, segundoNum);
Quando a chamada de função é executada, a função não recebe os nomes de argumentos primeiroNum e segundoNum. Em vez disso, recebe uma lista das posições de memória associadas a cada nome. Neste exemplo, a lista consiste nas posições 1010 1012
que são as posições atribuídas às variáveis de argumento primeiroNum e segundoNum, nesta ordem . E são essas as posições de memória associadas aos parâmetros formais. A primeira posição de memória ao primeiro parâmetro formal, a segunda posição de memória ao segundo parâmetro formal, e assim por diante. Em um diagrama, neste caso a correspondência é primeiroNum segundoNum
→ →
1010 1012
entrada1 entrada2
→ →
Quando os comandos da função são executados, o que quer que o corpo da função diga para fazer com um parâmetro formal é, na verdade, feito com a variável na posição de memória associada com aquele parâmetro formal. Nesse caso, as instruções no corpo da função getNumeros dizem que um valor deve ser armazenado no parâmetro formal entrada1 utilizando um comando cin, e assim o valor é armazenado na variável na posição de
96
Parâmetros e Sobrecarga
memória 1010 (que é a variável primeiroNum). De forma similar, as instruções no corpo da função getNumeros dizem que outro valor deve, então, ser armazenado no parâmetro formal entrada2 por meio de um comando cin e, dessa forma, aquele valor é armazenado na variável na posição de memória 1012 (que é a variável segundoNum). Assim, o que quer que a função instrua o computador a fazer com entrada1 e entrada2 é, na verdade, feito com as variáveis primeiroNum e segundoNum. Pode parecer que existam detalhes demais, ou, pelo menos, palavras demais nessa história. Se primeiroNum é a variável com a posição de memória 1010, por que insistimos em dizer “a variável na posição de memória 1010" em vez de simplesmente dizer “ primeiroNum”? Essa quantidade a mais de detalhes é necessária se os argumentos e os parâmetros formais contêm alguma coincidência de nomes criadora de confusão. Por exemplo, a função getNumeros possui parâmetros formais denominados entrada1 e entrada2. Suponha que você queira mudar o programa no Painel 4.2 para que ele utilize a função getNumeros com argumentos que também se chamem entrada1 e entrada2, e suponha que você queira fazer algo que não seja tão óbvio. Suponha, ainda, que você queira que o primeiro número digitado seja armazenado em uma variável chamada entrada2, e o segundo número digitado na variável chamada entrada1 — talvez porque o segundo número será processado primeiro ou porque é o número mais importante. Agora, vamos supor que às variáveis entrada1 e entrada2, que são declaradas na parte main do seu programa, tenham sido atribuídas as posições de memória 1014 e 1016. A função poderia ser assim: entrada1, entrada2; getNumeros(entrada2, entrada1); int
Observe a ordem dos argumentos.
Neste caso, se você disser entrada1”, não saberemos se você se refere à variável chamada entrada1 declarada na parte main do seu programa ou ao parâmetro formal entrada1. Entretanto, se à variável entrada1 declarada na função main do seu programa é atribuída a posição de memória 1014, a frase “a variável de posição de memória 1014” é inequívoca. Vamos analisar os detalhes dos mecanismos de substituição nesse caso. Nesta chamada o argumento correspondente ao parâmetro formal entrada1 é a variável entrada2, e o argumento correspondente ao parâmetro formal entrada2 é a variável entrada1. Isso pode parecer confuso para nós, mas não causa problema para o computador, já que este, na verdade, nunca “substitui entrada1 por entrada2” ou “substitui entrada2 por entrada1”. O computador simplesmente lida com posições de memória. O computador substitui o parâmetro formal entrada1 pela “variável na posição de memória 1016” e o parâmetro formal entrada2 pela “variável na posição de memória 1014”. “
FUNÇÃO
trocaValores
A função trocaValores definida no Painel 4.2 troca os valores armazenados nas duas variáveis. A descrição da função é dada pela seguinte declaração de função e comentário que a acompanha: void trocaValores(int& variavel1, int& variavel2); //Troca os valores da variavel1 e variavel2. Para ver como se espera que a função trabalhe, presuma que a variável primeiroNum tenha valor 5 e a variá vel segundoNum tenha valor 6 e considere a seguinte chamada de função: trocaValores(primeiroNum, segundoNum); Depois desta chamada de função, o valor de primeiroNum será 6 e o valor de segundoNum será 5. Como mostra o Painel 4.2, a definição da função trocaValores utiliza uma variável local chamada temp. A variável local é necessária. Você pode ser levado a pensar que a definição de função poderia ser simplificada para void trocaValores(int& variavel1, int& variavel2); { variavel1 = variavel2; Isto não funciona! variavel2 = variavel1; } Para verificar que esta definição alternativa não funciona, pense no que poderia acontecer com essa definição e a chamada de função trocaValores(primeiroNum, segundoNum); As variáveis primeiroNum e segundoNum substituiriam os parâmetros formais variavel1 e variavel2, de modo que, com esta definição de função incorreta, a chamada de função seria equivalente a: primeiroNum = segundoNum; segundoNum = primeiroNum;
Parâmetros
97
Este código não produz o resultado desejado. O valor de primeiroNum é fixado como igual ao valor de segundoNum, como deveria ser. Mas, então, o valor de segundoNum é fixado como igual ao valor alterado de primeiroNum, que agora é o valor original de segundoNum. Assim, o valor de segundoNum não é alterado. (Se isso não estiver claro para você, atribua valores específicos às variáveis primeiroNum e segundoNum e refaça o processo.) O que a função precisa fazer é salvar o valor original de primeiroNum, para que o valor não seja perdido. É para isso que se utiliza a variável local temp na definição de função correta. A definição correta é aquela apresentada no Painel 4.2. Quando esta versão correta é utilizada e a função é chamada com os argumentos primeiroNum e segundoNum, a chamada de função é equivalente ao código seguinte, que funciona corretamente: temp = primeiroNum; primeiroNum = segundoNum; segundoNum = temp;
PARÂMETROS DE REFERÊNCIA CONSTANTES Colocamos esta subseção aqui porque um dos objetivos deste livro é servir como referência. Se você estiver lendo o livro na seqüência, pode pular esta seção. O tópico será explicado com mais detalhes posteriormente. Se você colocar um const antes de um tipo de parâmetro chamado por referência, obterá um parâmetro chamado por referência que não pode ser alterado. Para os tipos que vimos até agora, isso não apresenta vantagens. Entretanto, vai se revelar um recurso bastante eficiente com vetores e parâmetros de tipo classe. Discutiremos esses parâmetros constantes quando falarmos em vetores e classes. ■
PENSE EM AÇÕES, NÃO EM CÓDIGO Embora possamos explicar como uma chamada de função atua em termos de substituir uma chamada de função pelo código, não é assim que costumamos pensar em uma chamada de função. Em vez disso, você deve pensar em uma chamada de função como uma ação. Por exemplo, considere a função trocaValores no Painel 4.2 e uma invocação como trocaValores(primeiroNum, segundoNum);
É mais fácil e mais claro pensar nessa chamada de função como a ação de trocar os valores desses dois argumentos. É muito mais obscuro pensar nela como o código temp = primeiroNum; primeiroNum = segundoNum; segundoNum = temp;
3. Qual é a saída do seguinte programa? #include using namespace std; void descubra(int& x, int y, int& z); int main( )
{ int a, b, c;
a = 10; b = 20; c = 30; descubra(a, b, c); cout << a << “ ” << b << “ ” << c << endl; return 0; } void descubra (int& x, int y, int & z)
{
98
Parâmetros e Sobrecarga
cout << x << " " << y << " " << z << endl; x = 1; y = 2; z = 3; cout << x << " " << y << " " << z << endl; }
4. Qual seria a saída do programa no Painel 4.2 caso se omitisse o “e” comercial (&) do primeiro parâmetro na declaração de função e do cabeçalho da função trocaValores? O “e” comercial não é removido do segundo parâmetro. Suponha que o usuário digite os números como no diálogo programa-usuário no Painel 4.2. 5. Escreva uma definição de função void para uma função chamada zeroAmbos que possui dois parâmetros chamados por referência, sendo ambos variáveis do tipo int, e fixa os valores de ambas as variáveis como 0. 6. Escreva uma definição de função void para uma função chamada somaImposto. A função chamada somaImposto possui dois parâmetros formais: taxaImposto, que é a quantia do imposto sobre vendas expressa em porcentagem e custo, que é o custo de um item antes do imposto. A função altera o valor de custo para incluir o imposto sobre vendas.
■
LISTA DE PARÂMETROS MISTOS
A definição de um parâmetro formal como sendo chamado por valor ou por referência é determinada pela presença ou não de um “e” comercial anexo à sua especificação de tipo. Se o “e” comercial estiver presente, o parâmetro formal é um parâmetro chamado por referência. Se não houver, é um parâmetro chamado por valor. PARÂMETROS E ARGUMENTOS Todos os termos diferentes que se referem a parâmetros e argumentos podem causar confusão. Entretanto, se você tiver em mente algumas questões simples, poderá lidar com esses termos com facilidade. 1. Os parâmetros formais para uma função são listados na declaração de função e usados no corpo da definição de função. Um parâmetro formal (de qualquer espécie) é um tipo de espaço em branco ou “guardador” de lugar que é preenchido com alguma coisa quando a função é chamada. 2. Um argumento é algo que é usado para preencher um parâmetro formal. Quando se escreve uma chamada de função, os argumentos são listados entre parênteses depois do nome da função. Quando a chamada de função é executada, os argumentos são conectados aos parâmetros formais. 3. Os termos chamada por valor e chamada por referência se referem ao mecanismo utilizado no processo de conexão. No método da chamada por valor, apenas o valor do argumento é utilizado. No mecanismo de chamada por valor, o parâmetro formal é uma variável local que é inicializada com o valor do argumento correspondente. No mecanismo de chamada por referência, o argumento é uma variável e toda a variável é utilizada. No mecanismo de c hamada por referência, a variável do argumento substitui o parâmetro formal, de modo que qualquer mudança no parâmetro formal é, na realidade, feita na variá vel do argumento.
É perfeitamente legítimo misturar parâmetros formais chamados por valor e por referência na mesma função. Por exemplo, o primeiro e o último argumentos formais na seguinte declaração de função são parâmetros formais chamados por referência e o do meio é um programa chamado por valor. void
muitoBom(int& par1,
int par2, double&
par3);
Parâmetros chamados por referência não estão restritos a funções void. Pode-se, também, utilizá-los em funções que retornam um valor. Assim, uma função com um parâmetro chamado por referência tanto poderia alterar o valor de uma variável dada quanto de um argumento e retornar um valor. QUE TIPO DE PARÂMETRO UTILIZAR O Painel 4.3 ilustra as diferenças entre como o compilador trata parâmetros formais chamados por valor e chamados por referência. Aos dois parâmetros par1Valor e par2Ref é atribuído um valor dentro do corpo da definição de função. No entanto, como são tipos diferentes de parâmetros, o efeito é diferente nos dois casos. par1Valor é um parâmetro chamado por valor, portanto é uma variável local. Quando a função é chamada da seguinte forma
Parâmetros
efetueIsso(n1, n2); a variável local par1Valor é inicializada com o valor de n1. Ou seja, a variável local par1Valor é inicializada como 1, e a variável n1 é então ignorada pela função. Como você pode ver pelo diálogo programa-usuário, o parâmetro formal par1Valor (que é a variável local) é fixado como 111 no corpo da função, e esse valor é apresentado na tela. Entretanto, o valor do argumento n1 não é alterado. Como exibido no diálogo programa-usuário, n1 reteve o valor de 1. Por outro lado, par2Ref é um parâmetro chamado por referência. Quando a função é chamada, o argumento da variável n2 (não apenas seu valor) substitui o parâmetro formal par2Ref. Então, quando o seguinte código é executado par2Ref = 222; é o mesmo que se este outro código fosse executado: n2 = 222; Portanto, o valor da variável n2 é alterado quando o corpo da função é executado; assim, como o diálogo mostra, o valor de n2 é alterado de 2 para 222 pela chamada de função. Se você não se esquecer da lição do Painel 4.3, é fácil decidir que mecanismo de parâmetro utilizar. Se você quer que uma função altere o valor de uma variável, então o parâmetro formal correspondente deve ser um parâmetro formal chamado por referência e deve ser assinalado com um sinal de “e” comercial, &. Em todos os outros casos, pode-se usar um parâmetro formal chamado por valor.
Painel 4.3
1 2 3 4
Comparando mecanismos de argumentos
//Ilustra a diferença entre parâmetros chamados por valor //e parâmetros chamados por referência. #include using namespace std;
5 void doStuff(int par1Value, int& par2Ref); 6 //par1Valor é um parâmetro chamado por valor formal e 7 //par2Ref é um parâmetro chamado por referência formal. 8 int main( ) 9 { int n1, n2; 10 11 12 n1 = 1; 13 n2 = 2; 14 doStuff(n1, n2); 15 cout << “n1 depois da chamada de função = ” << n1 << endl; 16 cout << “n2 depois da chamada de função = ” << n2 << endl; return 0; 17 18 } 19 void doStuff(int par1Value, int& par2Ref) 20 { 21 par1Value = 111; 22 cout << “par1Valor na chamada de função = ” 23 << par1Value << endl; 24 par2Ref = 222; 25 cout << “par2Ref na chamada de função = ” 26 << par2Ref << endl; 27 }
DIÁLOGO PROGRAMA-USUÁRIO par1Valor na chamada de função par2Ref na chamada de função = n1 depois da chamada de função n2 depois da chamada de função
= 111 222 = 1 = 222
99
100
Parâmetros e Sobrecarga
DESCUIDOS COM VARIÁVEIS LOCAIS Se você quer que uma função altere o valor de uma variável, o parâmetro formal correspondente deve ser um parâmetro chamado por referência e, portanto, deve ter o “e” comercial, &, anexado ao seu tipo. Se você omitir o “e” comercial, a função terá um parâmetro chamado por valor em vez de um parâmetro chamado por referência. Quando o programa for executado, você descobrirá que a chamada de função não altera o valor do argumento correspondente, porque um parâmetro formal chamado por valor é uma variável local. Se o parâmetro tiver seu valor alterado na função, então, como com qualquer variável local, essa alteração não exercerá efeito fora do corpo da função. Este é um erro que pode ser bastante difícil de perceber, porque o código parece certo. Por exemplo, o programa no Painel 4.4 é similar ao programa no Painel 4.2, a não ser pelo fato de o “e” comercial ter sido erroneamente omitido na função trocaValores. Em conseqüência, os parâmetros formais variavel1 e variavel2 são variáveis locais. As variáveis de argumento primeiroNum e segundoNum nunca substituem variavel1 e variavel2; variavel1 e variavel2 são, em vez disso, inicializadas com os valores de primeiroNum e segundoNum. Então, os valores de variavel1 e variavel2 são trocados, mas os valores de primeiroNum e segundoNum permanecem inalterados. A omissão de dois “ee” comerciais tornou o programa totalmente errado e, no entanto, ele parece quase idêntico ao programa correto e compilará e será executado sem qualquer mensagem de erro.
ESCOLHENDO NOMES DE PARÂMETROS FORMAIS As funções devem ser módulos independentes projetados separadamente do resto do programa. Em grandes projetos de programação, programadores diferentes são contratados para escrever funções diferentes. O programador deve escolher os nomes mais descritivos que encontrar para os parâmetros formais. Os argumentos que substituirão os parâmetros formais podem ser variáveis em outra função ou na função main. Essas variáveis também devem receber nomes descritivos, muitas vezes escolhidos por outra pessoa que não o programador que escreve a definição de função. Isso torna provável que alguns ou todos os argumentos tenham os mesmos nomes que alguns dos parâmetros formais. Isso é perfeitamente aceitável. Não importa que nomes sejam escolhidos para as variáveis que serão utilizadas como argumentos, esses nomes não causarão qualquer confusão com os nomes empregados para os parâmetros formais.
COMPRANDO PIZZA Nem sempre sai mais barato comprar o produto de maior tamanho. Isso é especialmente verdade em se tratando de pizzas. Nos Estados Unidos, os tamanhos das pizzas são dados pelo diâmetro da pizza em polegadas. Entretanto, a quantidade de pizza é determinada pela área da pizza, e a área da pizza não é proporcional ao diâmetro. A maioria das pessoas não consegue estimar com facilidade a diferença de área entre uma pizza de dez polegadas e uma de doze e, assim, não consegue decidir facilmente que tamanho é o melhor para se comprar — isto é, que tamanho tem o preço mais baixo por polegada quadrada (1polegada quadrada = 6,4516 cm 2). O Painel 4.5 mostra um programa que o consumidor pode utilizar para decidir qual dos dois tamanhos de pizza comprar. Observe que as funções getDados e forneceResultados possuem os mesmos parâmetros, mas como getDados alterará os valores de seus argumentos, seus parâmetros são chamados por referência. Por outro lado, forneceResultados só necessita dos valores de seus argumentos e, assim, seus parâmetros são chamados por valor. Note também que forneceResultados possui duas variáveis locais e que seu corpo de função inclui chamadas para as funções precoUnidade. Finalmente, observe que a função precoUnidade tem tanto as variáveis locais quanto uma variável local definidas como constantes.
Painel 4.4
1 2 3
Descuidos com variáveis locais ( parte 1 de 2)
//Programa para demonstrar parâmetros chamados por referência. #include using namespace std;
4 void getNumbers(int& input1, int& input2); 5 //Lê dois inteiros a partir do teclado. 6 void swapValues(int variable1, int variable2); 7 //Troca os valores de variable1 e variable2.
Esqueça o & aqui.
Parâmetros
Painel 4.4
Descuidos com variáveis locais ( parte 2 de 2)
8 void showResults(int output1, int output2); 9 //Mostra os valores de variable1 e variable2, nessa ordem. 10 int main( ) 11 { int firstNum, secondNum; 12 13 14 15 16 17 }
getNumbers(firstNum, secondNum); swapValues(firstNum, secondNum); showResults(firstNum, secondNum); return 0;
Esqueça o & aqui.
18 void swapValues(int variable1, int variable2) 19 { int temp; 20 Descuido com variáveis locais. 21 temp = variable1; 22 variable1 = variable2; 23 variable2 = temp; 24 } As definições de getNumbers e 25 showResults são as mesmas do Painel 4.2. 26
DIÁLOGO PROGRAMA-USUÁRIO Forneça dois inteiros: 5 6 Em ordem inversa os números são: 5 6
Painel 4.5
1 2 3
Erro devido ao descuido com variáveis locais.
Comprando pizza (parte 1 de 3)
//Determina qual dos dois tamanhos de pizza é o melhor para comprar. #include using namespace std;
4 void getData(int& smallDiameter, double& priceSmall, int& largeDiameter, double& priceLarge); 5 6 void giveResults(int smallDiameter, double priceSmall, 7 int largeDiameter, double priceLarge); 8 9 10 11
double unitPrice(int diameter, double price); //Fornece o preço por polegada quadrada de uma pizza. //Pré-condição: O parâmetro diameter é o diâmetro da pizza //em polegadas. O parâmetro price é o preço da pizza.
12 int main( ) 13 { int diameterSmall, diameterLarge; 14 15 double priceSmall, priceLarge;
As variáveis diameterSmall , diameterLarge, priceSmall e priceLarge são utilizadas para transportar dados da função getData para a função giveResults.
16 17
getData(diameterSmall, priceSmall, diameterLarge, priceLarge); giveResults(diameterSmall, priceSmall, diameterLarge, priceLarge);
18 19 }
return 0;
20 void getData(int& smallDiameter, double& priceSmall,
101
102
Parâmetros e Sobrecarga
Painel 4.5
Comprando pizza (parte 2 de 3)
21 int& largeDiameter, double& priceLarge) 22 { 23 cout << “Bem-vindo à União dos Consumidores de Pizza.\n”; 24 cout << “Informe o diâmetro de uma pizza pequena (em polegadas): ”; 25 cin >> smallDiameter; 26 cout << “Informe o preço de uma pizza pequena: $”; 27 cin >> priceSmall; 28 cout << “Informe o diâmetro de uma pizza grande (em polegadas): ”; 29 cin >> largeDiameter; 30 cout << “Informe o preço de uma pizza grande: $”; 31 cin >> priceLarge; 32 } 33 34 void giveResults(int smallDiameter, double priceSmall, 35 int largeDiameter, double priceLarge) Uma função chamada 36 { dentro de outra função. 37 double unitPriceSmall, unitPriceLarge; 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 }
unitPriceSmall = unitPrice(smallDiameter, priceSmall) ; unitPriceLarge = unitPrice(largeDiameter, priceLarge) ; cout.setf(ios::fixed); cout.setf(ios::showpoint); cout.precision(2); cout << “Pizza pequena:\n” << “Diâmetro = ” << smallDiameter << “ polegadas\n” << “Preço = $” << priceSmall << “ Por polegada quadrada = $” << unitPriceSmall << endl << “Pizza grande:\n” << “Diâmetro = ” << largeDiameter << “ polegadas\n” << “Preço = $” << priceLarge << “ Por polegada quadrada = $” << unitPriceLarge << endl; if (unitPriceLarge < unitPriceSmall) cout << “É melhor comprar a grande.\n”; else cout << “É melhor comprar a pequena.\n”; cout << “Buon Appetito!\n”;
57 double unitPrice(int diameter, double price) 58 { 59 const double PI = 3.14159; 60 double radius, area; 61 62 63 64 }
radius = diameter/static_cast(2); area = PI * radius * radius; return (price/area);
DIÁLOGO PROGRAMA-USUÁRIO
Bem-vindo à União dos Consumidores de Pizza. Informe o diâmetro de uma pizza pequena (em polegadas): 10 Informe o preço de uma pizza pequena: $7.50 Informe o diâmetro de uma pizza grande (em polegadas): 13 Informe o preço de uma pizza grande: $14.75 Pizza pequena: Diâmetro = 10 polegadas Preço = $7.50 Por polegada quadrada = $0.10 Pizza grande:
Sobrecarga e Argumentos-Padrão
Painel 4.5
103
Comprando pizza (parte 3 de 3)
Diâmetro = 13 polegadas Preço = $14.75 Por polegada quadrada = $0.11 É melhor comprar a pequena. Buon Appetito!
7. Qual seria a saída do programa no Painel 4.3 se você alterasse a declaração da função efetueIsso para a seguinte linha e alterasse o cabeçalho da função para combinar, de modo que o parâmetro formal par2Ref fosse alterado para um parâmetro chamado por valor? void efetueIsso( int par1Valor, int par2Valor);
4.2
Sobrecarga e Argumentos-Padrão — … e isso mostra que há trezentos e sessenta e quatro dias em que você poderia ganhar um presente de não- aniversário… — É verdade — reconheceu Alice. — E apenas um para presentes de aniversário, você sabe. Isto é a glória para você! — Não sei o que você quer dizer com “glória” — disse Alice. Humpty Dumpty sorriu com desdém. — Claro que você não sabe… até eu lhe contar. Quero dizer “este é um argumento arrasador para você!” — Mas “glória” não quer dizer “um argumento arrasador” — objetou Alice. — Quando eu uso uma palavra — disse Humpty Dumpty, em tom de desprezo —, ela quer dizer exatamente o que eu quero que signifique. Nem mais nem menos. — A questão — observou Alice — é se você pode fazer as palavras significarem tantas coisas diferentes. — A questão é — disse Humpty Dumpty — quem é que manda. Só isso. Lewis Carroll, Através do Espelho
O C++ permite que você dê duas ou mais definições diferentes para o mesmo nome de função, o que significa que você pode reutilizar nomes com forte apelo intuitivo em uma grande variedade de situações. Por exemplo, você poderia ter três funções chamadas max: uma que calcula o maior de dois números, outra que calcula o maior de três números e ainda outra que calcula o maior de quatro números. Diz-se que dar duas (ou mais) definições de função para o mesmo nome de função é sobrecarregar o nome da função. ■
INTRODUÇÃO À SOBRECARGA
Suponha que você escreva um programa que exija que se calcule a média de dois números. Você poderia usar a seguinte definição de função: double
med(double n1,
double n2)
{ return ((n1
+ n2)/2.0);
}
Agora suponha que seu programa exija também uma função para calcular a média de três números. Você poderia definir uma nova função chamada med3 da seguinte forma: double med3(double n1, double n2, double n3)
{ return ((n1
}
+ n2 + n3)/3.0);
104
Parâmetros e Sobrecarga
Isto funcionará, e em muitas linguagens de programação você não tem escolha exceto fazer algo assim. Entretanto, o C++ permite uma solução mais elegante. Em C++ você pode simplesmente utilizar o mesmo nome de função med para ambas as funções. Em C++, você faz a seguinte definição de função em lugar da definição de função med3: double med(double n1, double n2, double n3)
{ return ((n1 + n2 + n3)/3.0);
}
de modo que o nome de função med possua duas definições. Este é um exemplo de sobrecarga. Nesse caso, sobrecarregamos o nome de função med. O Painel 4.6 contém essas duas definições de função para med dentro de um programaamostra completo. Não deixe de observar que cada definição de função possui sua própria declaração (protótipo). O compilador pode dizer que definição de função utilizar verificando o número e o tipo dos argumentos em uma chamada de função. No programa do Painel 4.6, uma das funções chamadas med possui dois argumentos, e a outra, três. Quando há dois argumentos em uma chamada de função, aplica-se a primeira definição. Quando há três, aplica-se a segunda definição. SOBRECARREGANDO UM NOME DE FUNÇÃO Caso se tenha uma ou mais definições de função para o mesmo nome de função, isso se chama sobrecarga. Quando se sobrecarrega um nome de função, as definições da função devem ter números diferentes de parâmetros formais ou alguns parâmetros formais de tipos diferentes. Quando há uma chamada de função, o compilador utiliza a definição de função cujo número de parâmetros formais e tipos de parâmetros formais combina com os argumentos na chamada de função.
Sempre que são dadas duas ou mais definições para o mesmo nome de função, as várias definições de função devem ter diferentes especificações para seus argumentos; ou seja, quaisquer duas definições de função que possuam o mesmo nome de função devem usar números diferentes de parâmetros formais ou possuir um ou mais parâmetros de tipos diferentes (ou ambos). Observe que, quando se sobrecarrega um nome de função, as declarações para as duas definições diferentes devem diferir em seus parâmetros formais. Não se pode sobrecarregar uma função dando duas definições que diferem apenas no tipo do valor fornecido. Também não se pode sobrecarregar com base em qualquer diferença que não seja a da quantidade ou dos tipos de parâmetros. Não se pode sobrecarregar com base apenas em const ou em uma diferença de parâmetro chamado por valor versus parâmetro chamado por referência. 1 Painel 4.6
1 2 3
Sobrecarregando um nome de função ( parte 1 de 2)
//Ilustra a sobrecarga da função med. #include using namespace std;
4 double ave(double n1, double n2); 5 //Retorna a média de dois números n1 e n2. 6 7 double ave(double n1, double n2, double n3); 8 //Retorna a média de três números n1, n2 e n3. 9 int main( ) 10 { 11 cout << “A média de 2.0, 2.5 e 3.0 é ” 12 << ave(2.0, 2.5, 3.0) << endl; 13 14
cout << “A média de 4.5 e 5.5 é ” << ave(4.5, 5.5) << endl;
15 16 }
return 0;
1.
Alguns compiladores, na realidade, permitem que se sobrecarregue com base em const versus não const, mas você não deve contar com isso. O padrão C++ diz que isso não é permitido.
Sobrecarga e Argumentos-Padrão Painel 4.6
105
Sobrecarregando um nome de função ( parte 2 de 2) Dois argumentos
17 double ave(double n1, double n2) 18 { 19 return ((n1 + n2)/2.0); 20 }
Três argumentos
21 double ave(double n1, double n2, 22 { return ((n1 + n2 + n3)/3.0); 23 24 }
double n3)
DIÁLOGO PROGRAMA-USUÁRIO A média de 2.0, 2.5 e 3.0 é 2.5 A média de 4.5 e 5.5 é 5.0
Você já viu um tipo de sobrecarga no Capítulo 1 (revisto aqui) com o operador de divisão, /. Se ambos os operandos são do tipo int, como em 13/2, o valor retornado é o resultado da divisão de inteiros, nesse caso, 6. Por outro lado, se um operando ou ambos são do tipo double, o valor retornado é o resultado da divisão regular; por exemplo, 13/2.0 retorna o valor 6.5. Existem duas definições para o operador de divisão, /, e as duas definições diferem não por terem números diferentes de operandos, e sim por exigirem operandos de tipos diferentes. A única diferença entre sobrecarregar o / e sobrecarregar nomes de funções está em que os criadores da linguagem C++ já fizeram a sobrecarga de /, enquanto a sobrecarga dos seus nomes de função deve ser programada por você mesmo. O Capítulo 8 discute como sobrecarregar operadores como +, -, e assim por diante. ASSINATURA A assinatura de uma função é o nome da função com a seqüência de tipos na lista de parâmetros, não incluindo a palavra-chave const nem o “e” comercial, &. Quando você sobrecarrega um nome de função, as duas definições do nome de função devem ter assinaturas diferentes, utilizando essa definição de assinatura. (Alguns especialistas incluem const e/ou o “e” comercial como parte da assinatura, mas queríamos uma definição que funcionasse para explicar a sobrecarga.)
CONVERSÃO AUTOMÁTICA DE TIPO E SOBRECARGA Suponha que a seguinte definição de função ocorra em seu programa e que você não tenha sobrecarregado o nome de função mpg (então esta é a única definição de uma função chamada mpg). double mpg(double milhas, double galoes) //Retorna milhas por galão. { return (milhas/galoes); } Se você chamar a função mpg com argumentos de tipo int, então o C++ converterá automaticamente qualquer argumento de tipo int em um valor de tipo double. Dessa forma, a linha seguinte apresentará como saída 22.5 milhas por galão: cout << mpg(45, 2) << “ milhas por galão”; O C++ converte o 45 em 45.0 e o 2 em 2.0 e, então, executa a divisão 45.0/2.0 e obtém o valor a ser retornado, que é 22.5. Se uma função requer um argumento de tipo double e você lhe fornece um argumento de tipo int, o C++ converterá automaticamente o argumento int em um valor de tipo double. Isso é tão útil e natural que nem nos damos conta do processo. Entretanto, a sobrecarga pode interferir com essa conversão automática de tipos. Vamos ver um exemplo. Suponha que você tenha (tolamente) sobrecarregado o nome de função mpg, de modo que seu programa contenha a seguinte definição de mpg além da anterior: int mpg(int gols, int erros) //Retorna a Medida de Gols Perfeitos //que é calculada como (gols - erros).
106
Parâmetros e Sobrecarga
{ return
(gols - erros);
} Em um programa que contém ambas as definições para o nome de função mpg, a linha seguinte (infelizmente) apresentará como saída 43 milhas por galão (já que 43 é 45 - 2): cout << mpg(45, 2) << “ milhas por galão”; Quando o C++ vê a chamada de função mpg(45, 2), que possui dois argumentos de tipo int, o C++ primeiro procura por uma definição de função de mpg que possua dois parâmetros formais de tipo int. Se en-
contrar tal definição de função, o C++ utiliza essa definição de função. O C++ não converte um argumento int em um valor de tipo double a não ser que esta seja a única forma de encontrar uma definição de função que combine. O exemplo mpg ilustra mais uma questão a respeito da sobrecarga: você não deve usar o mesmo nome de função para duas funções não-relacionadas. Esse descuido no uso dos nomes de função acaba produzindo confusão.
8. Suponha que você tenha duas definições de função com as seguintes declarações: double placar(double tempo, double distancia); int placar(double pontos);
Que definição de função seria usada na seguinte chamada de função e por que seria esta a usada? ( x é do tipo double.) double placarFinal
= placar(x);
9. Suponha que você tenha duas definições de função com as seguintes declarações: double aResposta(double dado1, double dado2); double aResposta(double tempo, int contagem);
Que definição de função seria usada na seguinte chamada de função e por que seria esta a usada? ( x e y são do tipo double.) x = aResposta(y, 6.0);
■ REGRAS PARA RESOLVER SOBRECARGA
Se você usar sobrecarga para produzir duas definições do mesmo nome de função com listas de parâmetros similares (mas não idênticas), a interação da sobrecarga e da conversão automática de tipos pode causar confusões. As regras que o compilador utiliza para resolver qual das múltiplas definições sobrecarregadas de um nome de função aplicar a uma dada chamada de função são as seguintes: 1. Identidade perfeita: se o número e os tipos dos argumentos são exatamente iguais à definição (sem qualquer conversão automática de tipos), então essa é a definição usada. 2. Identidade com conversão automática de tipos: se não há uma identidade perfeita, mas há uma identidade por meio da conversão automática de tipos, então essa definição é usada. Se duas identidades são encontradas no estágio 1 ou se nenhuma for encontrada no estágio 1 e duas forem encontradas no estágio 2, então há uma situação ambígua e uma mensagem de erro é emitida. Por exemplo, a seguinte sobrecarga é de estilo dúbio, mas é perfeitamente válida: void
f(int n,
double m);
void f(double n, int m);
Entretanto, se você tiver também a invocação f(98, 99);
o compilador não sabe qual dos dois argumentos int converter para um valor de tipo double, e uma mensagem de erro é gerada. Para ver quão confusa e perigosa pode ser a situação, suponha que você acrescente a seguinte terceira sobrecarga: void f(int n, int m);
Sobrecarga e Argumentos-Padrão
107
Com o acréscimo dessa terceira sobrecarga, você não recebe mais uma mensagem de erro, já que agora há uma identidade perfeita. Obviamente, sobrecargas confusas como essas devem ser evitadas. As duas regras mencionadas funcionarão em quase todas as situações. De fato, se você precisa de regras mais precisas, deve reescrever seu código para ser mais compreensível. Entretanto, as regras exatas são ainda mais complicadas. Para cumprir com nosso objetivo de fazer um livro de referências, fornecemos as regras exatas a seguir. Alguns dos termos podem não fazer sentido para você até que se leiam mais capítulos deste livro, mas não se preocupe. As duas regras simples servirão até que você entenda as mais complicadas. 1. Identidade perfeita, como descrito anteriormente. 2. Identidades utilizando promoções dentro de tipos inteiros ou dentro de tipos de ponto flutuante, como de short para int ou float para double. (Observe que conversões de bool para int e char para int são consideradas promoções dentro dos tipos inteiros.) 3. Identidades utilizando outras conversões de tipos predefinidos, como de int para double. 4. Identidades utilizando conversões de tipos definidos pelo usuário (veja Capítulo 8). 5. Identidades utilizando elipses... (Este assunto não será abordado neste livro, e, se você não o utilizar, não haverá problemas.) Se duas identidades forem encontradas no primeiro estágio em que uma identidade é encontrada, então há uma situação ambígua e uma mensagem de erro será emitida. PROGRAMA “COMPRANDO PIZZA” REVISADO
A União dos Consumidores de Pizza gostou muito do programa que escrevemos para ela no Painel 4.5. Agora todos querem comprar a pizza proporcionalmente mais barata. Uma pizzaria desonesta costumava ganhar dinheiro enganando os consumidores, fazendo-os comprar a pizza mais cara, mas nosso programa pôs um fim nessa prática maléfica. Entretanto, os proprietários quiseram continuar com esse comportamento desprezível e inventaram um novo jeito de enganar os consumidores. Eles agora oferecem tanto pizzas redondas como retangulares. Eles sabem que o programa que escrevemos não consegue lidar com pizzas retangulares, e esperam poder confundir mais uma vez os consumidores. O Painel 4.7 é outra versão de nosso programa que compara uma pizza redonda e uma pizza retangular. Observe que o nome de função precoUnidade foi sobrecarregado para podermos aplicá-lo tanto a pizzas redondas quanto a retangulares.
Painel 4.7
Programa “comprando pizza” revisado ( parte 1 de 3)
1 2 3
//Determina se é melhor comprar a pizza redonda ou a retangular. #include using namespace std;
4 5 6 7
double unitPrice(int diameter, double price) ;
8 9 10 11
double unitPrice(int length, int width, double price) ;
//Retorna o preço por polegada quadrada de uma pizza redonda. //O parâmetro formal chamado diameter é o diâmetro da pizza //em polegadas. O parâmetro formal chamado price é o preço da pizza.
//Retorna o preço por polegada quadrada de uma pizza retangular //com dimensões de comprimento e largura em polegadas. //O parâmetro formal price é o preço de pizza.
12 int main( ) 13 { 14 int diameter, length, width; 15 double priceRound, unitPriceRound, 16 priceRectangular, unitPriceRectangular; 17 18 19 20 21
cout << “Bem-vindo à União dos Consumidores de Pizza.\n”; cout << “Informe o diâmetro em polegadas” << “ de uma pizza redonda: ”; cin >> diameter; cout << “Informe o preço de uma pizza redonda: $”;
108
Parâmetros e Sobrecarga
Painel 4.7
Programa “comprando pizza” revisado ( parte 2 de 3)
22 23 24 25 26 27
cin >> priceRound; cout << “Informe o comprimento e a largura em polegadas\n” << “de uma pizza retangular: ”; cin >> length >> width; cout << “Informe o preço de uma pizza retangular: $”; cin >> priceRectangular;
28 29 30
unitPriceRectangular = unitPrice(length, width, priceRectangular); unitPriceRound = unitPrice(diameter, priceRound);
31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
cout.setf(ios::fixed); cout.setf(ios::showpoint); cout.precision(2); cout << endl << “Pizza redonda: Diâmetro = ” << diameter << “ polegadas\n” << “Preço = $” << priceRound << “ Por polegada quadrada = $” << unitPriceRound << endl << “Pizza retangular: Largura = ” << length << “ polegadas\n” << “Pizza retangular: Width = ” << width << “ polegadas\n” << “Preço = $” << priceRectangular << “ Por polegada quadrada = $” << unitPriceRectangular << endl;
47 48 49 50 51
if (unitPriceRound
52 53 }
return 0;
< unitPriceRectangular) cout << “É melhor comprar a redonda.\n”;
else
cout << “É melhor comprar a retangular.\n”; cout << “Buon Appetito!\n”;
54 double unitPrice(int diameter, double price) 55 { 56 const double PI = 3.14159; 57 double radius, area; 58 59 radius = diameter/double(2); 60 area = PI * radius * radius; 61 return (price/area); 62 } 63 double unitPrice(int lenght, int width, double price) 64 { 65 double area = length * width; 66 return (price/area); 67 } DIÁLOGO PROGRAMA-USUÁRIO
Bem-vindo à União dos Consumidores de Pizza. Informe o diâmetro em polegadas de uma pizza redonda: Informe o preço de uma pizza redonda: $8.50 Informe o comprimento e a largura em polegadas de uma pizza retangular: 6 4 Informe o preço de uma pizza retangular: $7.55
10
Sobrecarga e Argumentos-Padrão
Painel 4.7
109
Programa “comprando pizza” revisado ( parte 3 de 3)
Pizza redonda: Diâmetro = 10 polegadas Preço = $8.50 Por polegada quadrada = $0.11 Pizza retangular: Comprimento = 6 polegadas Pizza retangular: Largura = 4 polegadas Preço = $7.55 Por polegada quadrada = $0.31 É melhor comprar a redonda. Buon Appetito!
ARGUMENTOS-PADRÃO Você pode especificar um argumento-padrão para um ou mais parâmetros chamados por valor em uma função. Se o argumento correspondente for omitido, então é substituído pelo argumento-padrão. Por exemplo, a função volume no Painel 4.8 calcula o volume de uma caixa a partir de seus comprimento, largura e altura. Se nenhuma altura for dada, presume-se que a altura seja 1. Se nem a largura nem a altura forem dadas, presume-se que ambas sejam 1. Observe que no Painel 4.8 os argumentos-padrão são dados na declaração de função, mas não na definição de função. Um argumento-padrão é dado na primeira vez que a função é declarada (ou definida, se isso ocorrer primeiro). Declarações subseqüentes ou uma definição a seguir não devem dar os argumentos-padrão novamente porque alguns compiladores considerarão isso um erro mesmo se os argumentos dados forem consistentes com aqueles dados anteriormente. Você pode ter mais de um argumento-padrão, mas todas as posições dos argumentos-padrão devem ser as posições mais à direita. Assim, para a função volume no Painel 4.8, poderíamos ter dado argumentos-padrão para o último, os últimos dois ou três parâmetros, mas qualquer outra combinação de argumentos-padrão não é permitida. ■
Painel 4.8 Argumentos-padrão ( parte 1 de 2)
1 2 3
Argumentos-padrão
#include using namespace std;
4 void showVolume(int length, int width = 1, int height = 1); 5 //Retorna o volume da caixa. 6 //Se a altura não for dada, presume-se que ela seja 1. 7 //Se nem a altura nem a largura forem dadas, presume-se que ambas sejam 1. 8 int main( ) 9 { 10 showVolume(4, 6, 2); 11 showVolume(4, 6); 12 showVolume(4); 13 14 }
Um argumento-padrão não deveria ser dado uma segunda vez.
return 0;
15 void showVolume(int length, int width, int height) 16 { 17 cout << “ Volume de uma caixa com\n” 18 << “Comprimento = ” << length << “, Largura = ” << width << endl 19 << “e Altura = ” << height 20 << “ é ” << length*width*height << endl; 21 } DIÁLOGO
PROGRAMA-USUÁRIO
Volume de uma caixa com Comprimento = 4, Largura = 6 e Altura = 2 é 48
110
Parâmetros e Sobrecarga
Painel 4.8
Argumentos-padrão ( parte 2 de 2)
Volume de uma caixa com Comprimento = 4, Largura = 6 e Altura = 1 é 24 Volume de uma caixa com Comprimento = 4, Largura = 1 e Altura = 1 é 4
Se houver mais de um argumento-padrão, quando a função é invocada você pode omitir argumentos a começar da direita. Por exemplo, observe que no Painel 4.8 há dois argumentos-padrão. Quando só um argumento é omitido, presume-se que seja o último argumento. Não há como se omitir o segundo argumento em uma invocação de volume sem omitir também o terceiro argumento. Argumentos-padrão possuem valor limitado, mas às vezes podem ser usados para refletir sua maneira de pensar a respeito de argumentos. Argumentos-padrão só podem ser usados com parâmetros chamados por valor. Eles não fazem sentido com parâmetros chamados por referência. Qualquer coisa que se possa fazer com argumentos-padrão também pode ser feita utilizando-se a sobrecarga, embora a versão com argumento-padrão provavelmente possa ser mais curta do que a com sobrecarga.
10. Esta pergunta tem a ver com o exemplo de programação intitulado Programa “Comprando Pizza” Re visado. Suponha que a pizzaria desonesta que está sempre tentando enganar os consumidores crie uma pizza quadrada. Será que você pode sobrecarregar a função precoUnidade para que esta possa calcular o preço por polegada quadrada de uma pizza quadrada, além do preço por polegada quadrada de uma pizza redonda? Por que sim ou por que não?
4.3
Testando e Depurando Funções Contemplei o infeliz — o monstro miserável que eu havia criado. Mary Wollstonecraft Shelley, Frankenstein
Esta seção apresenta algumas orientações gerais para testar programas e funções. ■ MACRO assert
Uma asserção é um comando que é verdadeiro ou falso. As asserções são utilizadas para documentar e verificar a correção de programas. Pré-condições e pós-condições, que discutimos no Capítulo 3, são exemplos de asserções. Quando expressa adequadamente e na sintaxe de C++, uma asserção é simplesmente uma expressão booleana. Se você converter uma asserção em uma expressão booleana, a macro predefinida assert pode ser usada para verificar se seu código satisfaz ou não a asserção. (Uma macro é bastante semelhante a uma função inline e é usada exatamente como uma função.) A macro assert é utilizada como uma função void que requer um parâmetro chamado por valor de tipo bool. Como uma asserção não passa de uma expressão booleana, isso significa que o argumento para assert é uma asserção. Quando a macro assert é invocada, seu argumento de asserção é avaliado. Se é avaliado como true, então nada acontece. Se o argumento é avaliado como false, o programa termina e uma mensagem de erro é enviada. Assim, chamadas à macro assert são uma forma compacta de incluir verificações de erro em seu programa. Por exemplo, a seguinte declaração de função retirada do Projeto de Programação 3: void calculaMoeda(int valorDaMoeda, int&
numero, int& quantiaRestante); //Pré-condição: 0 < valorDaMoeda < 100; 0 <= quantiaRestante < 100.
Testando e Depurando Funções
111
//Pós-condição: numero fixado como igual ao número máximo //de moedas de denominação valorDaMoeda centavos que possa ser obtido //a partir de quantiaRestante centavos. quantiaRestante diminui conforme //o valor das moedas, ou seja, diminui de numero*valorDaMoeda.
Você pode verificar se essa pré-condição se sustenta para uma invocação de função, como mostra o seguinte exemplo: assert((0 < moedaAtual) && (moedaAtual < 100) && (0 <= quantiaRestanteAtual) && (quantiaRestanteAtual < 100)); calculaMoeda(moedaAtual, numero, quantiaRestanteAtual);
Se a pré-condição não é satisfeita, seu programa terminará e enviará uma mensagem de erro. A macro assert está definida na biblioteca cassert, portanto qualquer programa que utilizar a macro assert deve conter a seguinte instrução: #include
Uma vantagem de utilizar assert é que você pode desativar invocações a assert. Você pode utilizar as invocações a assert em seu programa para depurá-lo e, depois, desativá-las para que os usuários não recebam mensagens de erro que talvez não entendam. Isso reduz os gastos de memória do seu programa. Para desativar todas as asserções #define NDEBUG em seu programa, acrescente #define NDEBUG antes da instrução de include, da seguinte forma: #define NDEBUG #include
Assim, se você inserir #define NDEBUG em seu programa depois de este estar totalmente depurado, todas as invocações a assert em seu programa serão desativadas. Se depois você alterar seu programa e precisar depurá-lo outra vez, pode ativar as invocações novamente apagando a linha #define NDEBUG (ou transformando-a em comentário). Nem todas as asserções de comentários podem ser facilmente traduzidas em expressões booleanas em C++. É mais provável que as pré-condições sejam mais facilmente traduzidas que as pós-condições. Assim, a macro assert não é uma panacéia para a depuração de suas funções, mas pode ser muito útil. ■ STUBS E DRIVERS
Cada função deveria ser projetada, codificada e testada como uma unidade separada do resto do programa. Quando se trata cada função como uma unidade à parte, transforma-se uma tarefa grande em várias menores, mais facilmente tratáveis. Mas como se testa uma função fora do programa para o qual foi projetada? Uma forma é escrever um programa especial para fazer os testes. Por exemplo, o Painel 4.9 mostra um programa para testar a função precoUnidade que foi usada no programa do Painel 4.5. Programas como esses são chamados de programas driver . Esses programas driver são ferramentas temporárias e podem ser bem pequenos. Não precisam ter rotinas de entrada muito complexas. Não precisam executar todos os cálculos que o programa final executará. Tudo o que precisam fazer é obter valores razoáveis para os argumentos da função da maneira mais simples possível — normalmente do usuário — e então executar a função e mostrar o resultado. Um loop, como no programa mostrado no Painel 4.9, permitirá que se teste novamente a função com diferentes argumentos sem ter de executar de novo o programa. Painel 4.9
1 2 3 4
Programa driver ( parte 1 de 2)
//Programa driver para a função unitPrice. #include using namespace std;
5 double unitPrice(int diameter, double price); 6 //Retorna o preço por polegada quadrada de uma pizza. 7 //Pré-condição: O parâmetro diameter é o diâmetro da pizza 8 //em polegadas. O parâmetro price é o preço da pizza. 9
int main( )
112
Parâmetros e Sobrecarga
Painel 4.9
10 { 11 12 13 14 15 16 17 18
Programa driver ( parte 2 de 2)
double diameter, price; char ans; do { cout << “Informe o diâmetro e o preço:\n”; cin >> diameter >> price; cout << “O preço por unidade é $” . << unitPrice(diameter, price) << endl;
19 20 21 22
cout << “Mais um teste? (s/n)”; cin >> ans; cout << endl; } while (ans == ’s’ || ans == ’S’);
23 return 0; 24 } 25 26 double unitPrice(int diameter, double price) 27 { 28 const double PI = 3.14159; 29 double radius, area; 30 31 32 33 }
radius = diameter/static_cast(2); area = PI * radius * radius; return (price/area);
DIÁLOGO PROGRAMA-USUÁRIO Informe o diâmetro e o preço: 13 14.75 O preço por unidade é: $0.111126 Mais um teste? (s/n): s Informe o diâmetro e o preço: 2 3.15 O preço por unidade é: $1.00268 Mais um teste? (s/n): n
Se você testar cada função separadamente, descobrirá a maioria dos erros em seu programa. Além disso, descobrirá que funções contêm os erros. Se você fosse testar apenas o programa inteiro, provavelmente descobriria que existe um erro, mas talvez não tivesse a menor idéia de onde ele estaria. Pior ainda, poderia pensar que sabe onde está e se enganar. Uma vez que tenha testado completamente uma função, você pode usá-la no programa driver para alguma outra função. Cada função deve ser testada em um programa no qual é a única função ainda não testada. Entretanto, é bom usar uma função já testada quando se testa alguma outra função. Se um erro for encontrado, você saberá que o erro está na função ainda não testada. Às vezes é impossível ou inconveniente testar uma função sem utilizar alguma outra função que não tenha ainda sido escrita ou testada. Nesse caso, você pode usar uma versão simplificada da função que falta ou que não foi testada. Essas funções simplificadas são chamadas de stubs. Os stubs não precisam, necessariamente, efetuar os cálculos corretos, e sim fornecer valores suficientes para o teste, e são tão simples que você pode ter confiança em seu desempenho. Por exemplo, eis aqui um possível stub para a função precoUnidade: //Um stub. A função final precisa ser escrita. double precoUnidade(int diametro, double preco)
Resumo do Capítulo
113
{ return(9.99);//Não
é correto mas é suficientemente bom para um stub.
}
Utilizar um esboço de programa com stubs permite que você teste e depois inicie o esboço básico do programa, em vez de escrever um programa completamente novo para testar cada função. Por essa razão, um esboço de programa com stubs costuma ser o método mais eficiente para testes. Uma abordagem comum é utilizar programas drivers para testar algumas funções básicas, como as de entrada e saída, e depois utilizar um programa com stubs para testar as funções restantes. Os stubs são substituídos por funções, um de cada vez: um stub é substituído por uma função completa e testado; quando essa função já foi completamente testada, outro stub é substituído por uma definição de função completa e assim por diante, até o programa final ser produzido. REGRA FUNDAMENTAL PARA O TESTE DE FUNÇÕES Cada função deve ser testada em um programa em que todas as outras funções já foram totalmente testadas e depuradas.
11. Qual é a regra fundamental para o teste de funções? Por que esta é uma boa forma de se testar funções? 12. O que é um programa driver? 13. O que é um stub? 14. Escreva um stub para a função cuja declaração é dada abaixo. Não escreva um programa inteiro, apenas o stub que entraria em um programa. ( Dica: o stub fica bem curto.) double chuvaProb(double pressao, double umidade, double temp); //Pré-condição: pressao é a pressão barométrica em polegadas de mercúrio. //umidade é a umidade relativa como porcentagem, e //temp é a temperatura em graus Fahrenheit. //Retorna a probabilidade de chuva, que é um número entre 0 e 1. //0 significa nenhuma probabilidade de chuva. 1 significa chuva com 100% de probabilidade.
■
■
■
■
■
■
■
Um parâmetro formal é um tipo de “guardador” de lugar que é preenchido com um argumento de função quando a função é chamada. Em C++, existem dois métodos para efetuar essa substituição, a chamada por valor e a por referência. Assim, há dois tipos básicos de parâmetro: chamados por valor e chamados por referência. Um parâmetro formal chamado por valor é uma variável local inicializada com o valor de seu argumento correspondente quando a função é chamada. Ocasionalmente, é útil empregar um parâmetro formal chamado por valor como uma variável local. No mecanismo de substituição da chamada por referência, o argumento deve ser uma variável e toda a variável é substituída pelo argumento correspondente. O modo de indicar um parâmetro chamado por referência em uma definição de função é anexar o símbolo de “e” comercial, &, ao tipo do parâmetro formal. (Um parâmetro chamado por valor é indicado pela ausência do “e” comercial.) Um argumento correspondente a um parâmetro chamado por valor não pode ser alterado por uma chamada de função. Um argumento correspondente a um parâmetro chamado por referência pode ser alterado por uma chamada de função. Se você quiser que uma função altere o valor de uma variável, é necessário usar um parâmetro chamado por referência. Podem-se dar múltiplas definições ao mesmo nome de função, desde que as diferentes funções com o mesmo nome possuam diferentes números de parâmetros ou algumas posições de parâmetro com tipos diferentes, ou ambos. Isso se chama sobrecarga do nome de função. Pode-se especificar um argumento-padrão para um ou mais parâmetros chamados por valor em uma função. Argumentos-padrão são sempre as posições de argumento mais à direita.
114
Parâmetros e Sobrecarga
■ ■
A macro assert auxilia a depuração de seus programas, verificando se as asserções se sustentam ou não. Toda função deve ser testada em um programa em que todas as outras funções já foram completamente testadas e depuradas.
RESPOSTAS DOS EXERCÍCIOS DE AUTOTESTE 1. Um parâmetro chamado por valor é uma variável local. Quando a função é invocada, o valor do argumento chamado por valor é calculado e o correspondente parâmetro chamado por valor (que é uma variá vel local) é inicializado com esse valor. 2. A função atuará bem. Essa resposta é completa e suficiente, mas queremos apresentar uma informação adicional: o parâmetro formal polegadas é um parâmetro chamado por valor, e portanto, como foi discutido, é uma variável local. Assim, o valor do argumento não será alterado. 3. 10 20 30 1 2 3 1 20 3 4. Forneça dois inteiros: 5 10 Em ordem inversa os números são: 5 5 5. void zeroAmbos(int& n1, int& n2) { n1 = 0; n2 = 0; } 6. void somaImposto(double taxaImposto, double& custo) { custo = custo + (taxaImposto/100.0)*custo; } A divisão por 100 é para converter a porcentagem em uma fração. Por exemplo, 10% é 10/100.0, ou um décimo do custo. 7. par1Valor na chamada de função = 111 par2Ref na chamada de função = 222 n1 depois da chamada de função = 1 Diferente n2 depois da chamada de função = 2 8. Seria usada a definição de função com um parâmetro, porque a chamada de função tem apenas um parâmetro. 9. A primeira seria usada porque é uma identidade perfeita, já que há dois parâmetros de tipo double. 10. Isso não pode ser feito (pelo menos não de um jeito aceitável). O jeito natural de se representar uma pizza quadrada e redonda é o mesmo. Cada uma é naturalmente representada como um número, que, para a pizza redonda, é o raio e, para a pizza quadrada, é o comprimento de um lado. Em ambos os casos, a função precoUnidade precisa ter um parâmetro formal de tipo double para o preço e um parâmetro formal de tipo int para o tamanho (raio ou lado). Assim, as duas declarações de função teriam o mesmo número de tipos de parâmetros formais. (Especificamente, ambas teriam um parâmetro formal de tipo double e um parâmetro formal de tipo int.) Logo, o compilador não seria capaz de decidir que definição usar. Você ainda pode derrotar a estratégia da pizzaria desonesta definindo duas funções, mas elas precisariam ter nomes diferentes. 11. A regra fundamental para testar funções é que cada função deve ser testada em um programa em que todas as outras funções já foram totalmente testadas e depuradas. Esta é uma boa forma de se testar uma função porque, se você seguir essa regra, quando encontrar um erro, saberá qual função o contém. 12. Um programa driver é um programa escrito com o único propósito de testar uma função. 13. Um stub é uma versão simplificada de uma função usada no lugar da função para que outras funções possam ser testadas. 14. //ISTO É APENAS UM STUB double chuvaProb(double pressao, double umidade, double temp) {
Projetos de Programação return 0.25;
115
//Não é correto, //mas serve para fazer o teste.
}
PROJETOS DE PROGRAMAÇÃO 1. Escreva um programa que converta da notação de 24 horas para a notação de 12 horas. Por exemplo, o programa deve converter 14:25 em 2:25 P.M. A entrada é dada em dois inteiros. Deve haver pelo menos três funções: uma para a entrada, uma para fazer a conversão e uma para a saída. Registre a informação A.M./P.M. como um valor de tipo char, ’A’ para A.M. e ’P’ para P.M. Assim, a função para efetuar as conversões terá um parâmetro formal chamado por referência de tipo char para registrar se é A.M. ou P.M. (A função terá outros parâmetros também.) Inclua um loop que permita que o usuário repita esse cálculo para novos valores de entrada todas as vezes que desejar, até o usuário dizer que deseja encerrar o programa. 2. A área de um triângulo arbitrário pode ser calculada por meio da fórmula area = √ s (s − a )(s − b )(s − c ) onde a, b e c são as medidas dos lados e s é o semiperímetro. s = (a + b + c ) /2 Escreva uma função void que utilize cinco parâmetros: três parâmetros chamados por valor que forneçam a medida dos lados e dois parâmetros chamados por referência que calculem a área e o perímetro ( não o semiperímetro ). Torne sua função robusta. Observe que nem todas as combinações de a, b e c produzem um triângulo. Sua função deve corrigir resultados para dados legais e resultados coerentes para combinações ilegais. 3. Escreva um programa que diga quantas moedas retornar para qualquer quantia de 1 a 99 centavos. Por exemplo, se a quantia é 86 centavos, a saída deve ser algo parecido com: 86 centavos podem ser fornecidos como 3 de 25 centavo(s), 1 de 10 centavo(s) e 1 de 1 centavo(s)
Utilize denominações para moedas de 25 centavos, 10 centavos e 1 centavo. Não utilize as moedas de 50 centavos nem de 5 centavos. Seu programa utilizará a seguinte função (entre outras): void calculaMoedas(int valorDaMoeda, int&
numero, int& quantiaRestante); //Pré-condição: 0 < valorDaMoeda < 100; 0 <= quantiaRestante < 100. //Pós-condição: número fixado como igual ao número máximo //de moedas de denominação valorDaMoeda centavos que possa ser obtido //a partir de quantiaRestante centavos. quantiaRestante diminui conforme //o valor das moedas, ou seja, diminui de numero*valorDaMoeda. Por exemplo, suponha que o valor da variável quantiaRestante seja 86. Então, depois da seguinte chamada, o valor de numero será 3 e o valor da quantiaRestante será 11 (porque se você tira 75 de 86, restam 11): calculaMoedas(25, numero, quantiaRestante);
Inclua um loop que permita ao usuário repetir esse cálculo para novos dados de entrada até o usuário dizer que deseja encerrar o programa. (Dica: utilize divisão de inteiros e o operador % para implementar essa função.) 4. Escreva um programa que leia um comprimento em pés e polegadas e apresente a saída equivalente em metros e centímetros. Utilize pelo menos três funções: uma para entrada, uma ou mais para o cálculo e uma para a saída. Inclua um loop que permita ao usuário repetir esse cálculo para novos dados de entrada até o usuário dizer que deseja encerrar o programa. Existem 0.3048 metros em um pé, 100 centímetros em um metro e 12 polegadas em um pé. 5. Escreva um programa como o do exercício anterior que converta metros e centímetros em pés e polegadas. Utilize funções para as subtarefas. 6. (Você deve fazer os dois projetos de programação anteriores antes de fazer este.) Escreva um programa que combine as funções dos dois projetos de programação anteriores. O programa pergunta ao usuário se deseja converter pés e polegadas em metros e centímetros ou metros e centímetros em pés e polegadas.
116
Parâmetros e Sobrecarga
7.
8. 9.
10.
Então, o programa efetua a conversão desejada. Faça com que o usuário responda digitando o inteiro 1 para um tipo de conversão e 2 para o outro. O programa lê a resposta do usuário e executa o comando if-else. Cada ramificação do comando if-else será uma chamada de função. As duas funções chamadas no comando if-else terão definições de função bastante similares às dos programas dos dois projetos de programação anteriores. Assim, serão definições de função bastante complexas que chamam outras funções. Inclua um loop que permita ao usuário repetir esse cálculo para novos dados de entrada até o usuário dizer que deseja encerrar o programa. Escreva um programa que leia o peso em libras (1 libra = 453,59 gramas) e onças (1 onça = 28,34 gramas) e apresente como saída o equivalente em quilogramas e gramas. Use pelo menos três funções: uma para entrada, uma ou mais para o cálculo e uma para a saída. Inclua um loop que permita ao usuário repetir esse cálculo para novos dados de entrada até o usuário dizer que deseja encerrar o programa. Existem 2.2046 libras em um quilograma, 1.000 gramas em um quilograma e 16 onças em uma libra. Escreva um programa como o do exercício anterior que converta quilogramas e gramas em libras e onças. Utilize funções para as subtarefas. (Você deve fazer os dois projetos de programação anteriores antes de fazer este.) Escreva um programa que combine as funções dos dois projetos de programação anteriores. O programa pergunta ao usuário se deseja converter libras e onças em quilogramas e gramas ou quilogramas e gramas em libras e onças. Então o programa efetua a conversão desejada. Faça com que o usuário responda digitando o inteiro 1 para um tipo de conversão e 2 para o outro. O programa lê a resposta do usuário e executa o comando ifelse. Cada ramificação do comando if-else será uma chamada de função. As duas funções chamadas no comando if-else terão definições de função bastante similares às dos programas dos dois projetos de programação anteriores. Assim, serão definições de função bastante complexas que chamam outras funções. Inclua um loop que permita ao usuário repetir esse cálculo para novos dados de entrada até o usuário dizer que deseja encerrar o programa. (Você deve fazer os Projetos de Programação 6 e 9 antes de fazer este.) Escreva um programa que combine as funções dos dois Projetos de Programação 6 e 9. O programa pergunta ao usuário se deseja converter comprimentos ou pesos. Se o usuário escolher comprimentos, o programa pergunta ao usuário se deseja converter pés (1 pé = 30,5 cm) e polegadas (1 polegada = 2,54 cm) em metros e centímetros ou metros e centímetros em pés e polegadas. Se o usuário escolher peso, uma pergunta similar é feita a respeito de libras, onças, quilogramas e gramas. Assim, o programa efetua a conversão desejada. Faça com que o usuário responda digitando o inteiro 1 para um tipo de conversão e 2 para o outro. O programa lê a resposta do usuário e executa o comando if-else. Cada ramificação do comando if-else será uma chamada de função. As duas funções chamadas no comando if-else terão definições de função bastante similares às dos programas dos Projetos de Programação 6 e 9. Observe que seu programa terá comandos if-else inseridos dentro de comandos if-else, mas apenas de maneira indireta. O comando if-else exterior incluirá duas chamadas de função, como suas duas ramificações. Essas duas chamadas de função, por sua vez, incluirão um comando if-else, mas você não precisa pensar nisso. São apenas chamadas de função e os detalhes estão na caixa preta que você cria quando define essas funções. Se você tentar criar uma ramificação de quatro caminhos, provavelmente está na pista errada. Você só precisa pensar em ramificações de dois caminhos (embora o programa inteiro se ramifique, no fim das contas, em quatro casos). Inclua um loop que permita ao usuário repetir esse cálculo para novos dados de entrada, até o usuário dizer que deseja encerrar o programa.
Vetores Vetores Capítulo 5Vetores É um erro capital teorizar antes de ter os dados. Sir Arthur Conan Doyle, Escândalo na Boêmia (Sherlock Holmes)
INTRODUÇÃO Um vetor é usado para processar uma coleção de dados de mesmo tipo, como uma lista de temperaturas ou uma lista de nomes. Este capítulo aborda os princípios básicos de definição e utilização de vetores em C++ e apresenta muitas das técnicas básicas para projetar algoritmos e programas que empregam vetores. Se quiser, pode ler o Capítulo 6 e quase todo o Capítulo 7, que trata de classes, antes de ler este capítulo. A única seção daqueles capítulos que utiliza conceitos deste é a Seção 7.3, que apresenta os vectors . 5.1
Introdução aos Vetores
Suponha que desejemos escrever um programa que leia cinco notas de provas e execute algumas manipulações sobre essas notas. Por exemplo, o programa poderia calcular a maior nota de prova e depois apresentar como saída a quantidade que faltou para cada uma das outras provas se igualar à nota mais alta. Esta não é conhecida até que todas as cinco notas sejam lidas. Dessa forma, todas as cinco notas devem ser armazenadas para que, depois que a mais alta seja calculada, cada nota possa ser comparada com ela. Para conservar as cinco notas, precisaremos de algo equivalente a cinco variáveis de tipo int. Poderíamos usar cinco variáveis individuais de tipo int, mas cinco variáveis são difíceis de controlar e depois poderemos querer mudar nosso programa para lidar com 100 notas; com certeza, 100 variáveis é algo impraticável. Um vetor é a solução ideal. Um vetor comporta-se como uma lista de variáveis com um mecanismo uniforme de nomeação que pode ser declarado em uma única linha de código simples. Por exemplo, os nomes para as cinco variáveis individuais que precisamos poderiam ser nota[0], nota[1], nota[2], nota[3] e nota[4]. A parte que não muda, nesse caso, nota, é o nome do vetor. A parte que pode mudar é o inteiro entre colchetes, [ ]. ■ DECLARANDO E REFERENCIANDO VETORES
Em C++, um vetor que consiste em cinco variáveis de tipo seguinte forma: int
int pode
ser declarado da
nota[5];
É como declarar as cinco variáveis seguintes como sendo todas de tipo nota[0], nota[1], nota[2], nota[3], nota[4]
int:
118
Vetores
É possível se referir de diversas formas a essas variáveis individuais que juntas constituem o vetor. Nós as chamaremos de variáveis indexadas, embora muitas vezes também sejam chamadas variáveis subscritas ou elementos do vetor. O número entre colchetes é chamado índice ou subscrito. Em C++, os índices são numerados a começar do 0, não do 1 nem de outro número que não seja o 0. O número de variáveis indexadas em um vetor é chamado de tamanho declarado do vetor, ou às vezes simplesmente de tamanho do vetor. Quando um vetor é declarado, o tamanho do vetor é dado entre colchetes depois do nome do vetor. As variáveis indexadas são, então, numeradas (também utilizando colchetes), começando do 0 e terminando com um inteiro que seja um número inferior ao tamanho do vetor. Em nosso exemplo, as variáveis indexadas eram do tipo int, mas um vetor pode ter variáveis indexadas de qualquer tipo. Por exemplo, para declarar um vetor com variáveis indexadas de tipo double, é só usar o nome de tipo double em vez de int na declaração do vetor. Todas as variáveis indexadas para um vetor, contudo, são de mesmo tipo. Esse tipo é chamado de tipo-base de um vetor. Assim, em nosso exemplo do vetor nota, o tipo-base é int. Podem-se declarar vetores e variáveis regulares juntos. Por exemplo, a linha seguinte declara as duas variáveis int proximo e max, além do vetor nota: int proximo, nota[5], max;
Uma variável indexada como nota[3] pode ser usada em qualquer lugar em que uma variável ordinária de tipo int possa ser usada. Não confunda as duas formas de se utilizar os colchetes, [ ], com um nome de vetor. Em uma declaração, como int nota[5];
o número entre colchetes especifica quantas variáveis indexadas o vetor possui. Quando usado em qualquer outro lugar, o número entre colchetes especifica a que variável indexada se refere. Por exemplo, nota[0] até nota[4] são variáveis indexadas do vetor declarado acima. O índice dentro dos colchetes não precisa ser fornecido como uma constante inteira. Pode-se usar qualquer expressão entre colchetes, desde que essa expressão seja avaliada como um dos inteiros de 0 até o inteiro um número inferior ao tamanho do vetor. Por exemplo, o trecho seguinte estabelecerá o valor de nota[3] como igual a 99: int n = 2;
nota[n + 1] = 99;
Embora possam parecer diferentes, nota[n + 1] e nota[3] são a mesma variável indexada no código acima, porque n + 1 é calculado como 3. A identidade de uma variável indexada, como nota[i], é determinada pelo valor de seu índice, que, neste exemplo, é i. Assim, você pode escrever programas que dizem algo como “faça isso e aquilo com a iésima variável indexada”, em que o valor de i é calculado pelo programa. Por exemplo, o programa no Painel 5.1 lê notas e as processa da forma descrita no início deste capítulo. Painel 5.1
Programa utilizando um vetor ( parte 1 de 2)
1 2 3 4
//Lê as cinco notas e mostra como cada //uma difere da nota mais alta. #include using namespace std;
5 6 7
int main( )
8 9 10 11 12 13
{ int i, score[5] , max;
cout << "Forneça 5 notas:\n"; cin >> score[0]; max = score[0]; for (i = 1; i < 5; i++) { cin >> score[i];
Introdução aos Vetores Painel 5.1
119
Programa utilizando um vetor ( parte 2 de 2)
14 15 16 17
if (score[i]
> max) max = score[i]; //max é o maior entre os valores score[0],..., score[i]. }
18 19 20 21 22 23
cout << "A nota mais alta é " << max << endl << "As notas e suas diferenças\n" << "em relação à nota mais alta são:\n"; for (i = 0; i < 5; i++) cout << score[i] << " inferior a " << (max - score[i]) << endl;
24 25 }
return 0;
DIÁLOGO PROGRAMA-USUÁRIO Forneça 5 notas: 5 9 2 10 6
A nota mais alta é 10 As notas e suas diferenças em relação à nota mais alta são: 5 inferior a 5 9 inferior a 1 2 inferior a 8 10 inferior a 0 6 inferior a 4
USE LOOPS for
COM VETORES O segundo loop for no Painel 5.1 ilustra uma forma comum de se percorrer um vetor for (i = 0; i < 5; i++) cout << nota[i] << " inferior a " << (max - nota[i]) << endl; O comando for é ideal para a manipulação em vetores.
Í NDICES DE VETORES SEMPRE COMEÇAM COM ZERO Os índices de um vetor sempre começam com 0 e terminam com o inteiro que seja igual ao tamanho do vetor menos um.
USE UMA CONSTANTE DEFINIDA PARA O TAMANHO DE UM VETOR Leia novamente o programa no Painel 5.1. Ele só funciona para classes que tenham exatamente cinco alunos. A maioria das classes não tem exatamente cinco alunos. Uma forma de tornar o programa mais versátil é utilizar uma constante definida para o tamanho de cada vetor. Por exemplo, o programa no Painel 5.1 poderia ser reescrito para utilizar a seguinte constante definida: const int NUMERO_DE_ALUNOS = 5; A linha com a declaração do vetor seria, então int i, nota[NUMERO_DE_ALUNOS], max; Claro que todos os lugares no programa em que o tamanho do vetor é 5 também deveriam ser alterados para ter NUMERO_DE_ALUNOS em vez de 5. Se essas mudanças forem feitas no programa (ou, melhor ainda, se o programa for escrito assim), então o programa pode ser revisado para trabalhar com qualquer número de alunos simplesmente trocando a linha que define a constante NUMERO_DE_ALUNOS .
120
Vetores
Observe que você não pode utilizar uma variável para o tamanho do vetor, como no trecho: cout << "Informe o número de alunos:\n"; cin >> numero; int nota[numero]; //ILEGAL EM MUITOS COMPILADORES! Alguns compiladores, mas não todos, permitirão que você especifique um tamanho de vetor com uma variá vel desta forma. Entretanto, em nome da portabilidade, você não deve fazer isso, mesmo que o seu compilador o permita. (No Capítulo 10, discutiremos um tipo diferente de vetor cujo tamanho pode ser determinado quando o programa é executado.)
DECLARAÇÃO DE VETOR SINTAXE Nome_Tipo Nome_Vetor[Tamanho_Declarado];
EXEMPLOS int grandeVetor[100]; double
a[3];
double b[5]; char serie[10],
serieUm; Uma declaração de vetor da forma mostrada acima definirá Tamanho_Declarado variáveis indexadas, ou seja, as variáveis indexadas de Nome_Vetor[0] até Nome_Vetor[Tamanho_Declarado-1]. Cada variável indexada é uma variável de tipo Nome_Tipo. O vetor a consiste nas variáveis indexadas a[0], a[1] e a[2], todas de tipo double. O vetor b consiste nas variáveis indexadas b[0], b[1], b[2], b[3] e b[4], todas também de tipo double. Você pode combinar declarações de vetor com declarações de simples variáveis, como na variável serieUm acima.
VETORES NA MEMÓRIA Antes de tratarmos de como os vetores são representados na memória de um computador, vamos ver primeiro como uma variável simples (de tipo int ou double, por exemplo) é representada na memória de um computador. A memória de um computador consiste em uma lista de posições numeradas chamadas bytes.1 O número de um byte é conhecido como endereço. Uma variável simples é implementada como uma porção de memória que consiste em alguns números de bytes consecutivos. O número de bytes é determinado pelo tamanho da variável. Assim, uma variável simples na memória é descrita por dois pedaços de informação: um endereço na memória (dando a posição do primeiro byte para aquela variável) e o tipo da variável, que diz quantos bytes de memória a variável requer. Quando falamos em endereço de uma variável , é a esse endereço que nos referimos. Quando seu programa armazena um valor na variável, o que realmente acontece é que o valor (codificado como zeros e uns) é colocado naqueles bytes de memória atribuídos àquela variável. De forma similar, quando uma variável é dada como um argumento (chamada por referência) a uma função, é o endereço da variável que, na realidade, é transmitido para a função que faz a chamada. Agora vamos tratar da questão de como os vetores são armazenados na memória. Variáveis indexadas de vetores são representadas na memória da mesma forma que as variáveis comuns, mas com vetores a história é um pouco mais complicada. As posições para as diversas variáveis indexadas de vetores são sempre conjuntas umas às outras na memória. Por exemplo, considere a seguinte declaração: ■
int
a[6];
Quando se declara esse vetor, o computador reserva memória suficiente para abrigar seis variáveis de tipo int. Além disso, o computador sempre coloca essas variáveis, uma depois da outra, na memória. Então o computador se lembra do endereço das variáveis indexadas a[0], mas não se lembra do endereço de nenhuma outra variável indexada. Quando seu programa precisa do endereço de alguma outra variável indexada nesse vetor, o computador calcula o endereço para essa outra variável indexada a partir do endereço de a[0]. Por exemplo, se você começa no endereço de a[0] e conta posições de memória suficientes para três variáveis de tipo int, então estará no endereço de a[3]. Para obter o endereço de a[3], o computador começa no endereço de a[0] (que é um número). 1.
Um byte consiste em oito bits, mas o tamanho exato de um byte não é importante para esta discussão.
Introdução aos Vetores
121
Então, o computador adiciona o número de bytes necessário para abrigar três variáveis de tipo int ao número do endereço de a[0]. O resultado é o endereço de a[3]. Essa implementação é apresentada em diagrama no Painel 5.2. Muitas das peculiaridades dos vetores em C++ só podem ser entendidas em relação a esses detalhes sobre a memória. Por exemplo, na próxima seção "Armadilha", utilizaremos esses detalhes para explicar o que acontece quando seu programa utiliza um índice ilegal. Í NDICE DE VETOR FORA DO INTERVALO
O erro mais comum de programação é feito quando se usam vetores tentando referenciar um índice não existente. Por exemplo, considere a seguinte declaração de vetor: int a[6];
Quando se usa o vetor a, toda expressão de índices deve ter como resultado um dos inteiros de 0 a 5. Por exemplo, se seu programa contém a variável indexada a[i], o i deve ser avaliado como um dos seis inteiros 0, 1, 2, 3, 4 ou 5. Se i é avaliado como qualquer outra coisa, é um erro. Quando uma expressão de índice é avaliada como algum valor além daqueles permitidos pela declaração de vetor, diz-se que o índice está fora do intervalo ou simplesmente que é ilegal. Na maioria dos sistemas, o resultado de um índice ilegal é que seu programa simplesmente fará algo errado, às vezes desastrosamente errado, e fará isso sem lhe dar qualquer aviso. Por exemplo, suponha que seu sistema seja típico, o vetor a seja declarado como acima e seu programa contenha a seguinte declaração: a[i] = 238;
Agora suponha que o valor de i, infelizmente, seja 7. O computador procede como se a[7] fosse uma variável indexada legal. O computador calcula o endereço onde a[7] deveria estar (se existir um a[7]) e coloca o valor 238 nessa posição de memória. Entretanto, não existe a variável indexada a[7] e a memória que recebe esse 238 provavelmente pertence a alguma outra variável, talvez uma variável chamada outraCoisa. Assim, o valor de outraCoisa é alterado inadvertidamente. Essa situação é ilustrada no Painel 5.2. Índices de vetor geralmente saem do intervalo na primeira ou última iteração de um loop que percorre o vetor. Desse modo, é preciso verificar cuidadosamente todos os loops que percorrem vetores para ter certeza de que iniciem e terminem em índices legais.
Painel 5.2
Vetor na memória
Endereço de a [0]
Nesse computador cada variável indexada utiliza 2 bytes, então a[3] começa 2 x 3 = 6 bytes depois do início de a [0]
Não existe a variável indexada a[6], mas, se houvesse uma, ficaria aqui. Não existe a variável indexada a[7], mas, se houvesse uma, ficaria aqui.
122
Vetores
■ INICIALIZANDO VETORES
Um vetor pode ser inicializado quando é declarado. Quando se inicializa o vetor, os valores das diversas variá veis indexadas ficam entre chaves e separados com vírgulas. Por exemplo: int criancas[3]
= {2, 12, 1};
A declaração anterior é equivalente ao seguinte código: int criancas[3];
criancas[0] = 2; criancas[1] = 12; criancas[2] = 1;
Se você listar menos valores do que variáveis indexadas, esses valores serão usados para inicializar as primeiras variáveis indexadas e as variáveis indexadas restantes serão inicializadas com o valor zero do tipo base de vetor. Nessa situação, as variáveis indexadas sem inicializadores são inicializadas como zero. Entretanto, vetores sem inicializadores e outras variáveis declaradas dentro de uma definição de função, inclusive a função main de um programa, não são inicializados. Apesar de as variáveis indexadas de vetor (e outras variáveis) poderem, às vezes, ser automaticamente inicializadas como zero, não se pode e não se deve contar com isso. Caso você inicialize um vetor em sua declaração, pode omitir o tamanho do vetor e este será automaticamente declarado com o tamanho mínimo necessário para os valores de inicialização. Por exemplo, a declaração seguinte int b[]
= {5, 12, 11};
é equivalente a int b[3]
= {5, 12, 11};
1. Descreva a diferença do significado de int a[5]; e do significado de a[4]. Qual é o significado do [5] e do [4] em cada caso? 2. Na declaração do vetor nota[5]; identifique o seguinte: double
a. O nome do vetor b. O tipo-base c. O tamanho declarado do vetor d. O intervalo de valores que um índice que se refira a esse vetor pode ter e. Uma das variáveis indexadas (ou elementos) desse vetor 3. Identifique os erros nas seguintes declarações de vetor. a. int x[4] = { 8, 7, 6, 4, 3 }; b. int x[] = { 8, 7, 6, 4 }; c. const int TAMANHO = 4; int x[TAMANHO];
4. Qual é a saída do seguinte código? char simbolo
[3] = {’a’, ’b’, ’c’}; indice = 0; indice < 3; indice++) cout << simbolo[indice];
for (int
5. Qual é a saída do seguinte código? double a[3]
= {1.1, 2.2, 3.3}; cout << a[0] << " " << a[1] << " " << a[2] << endl; a[1] = a[2]; cout << a[0] << " " << a[1] << " " << a[2] << endl;
6. Qual é a saída do seguinte código? int i,
temp[10];
Vetores em Funções
for (i
= 0; i < 10; temp[i] = 2*i; for (i = 0; i < 10; cout << temp[i] cout << endl; for (i = 0; i < 10; cout << temp[i]
123
i++) i++) << " "; i = i + 2) << " ";
7. O que há de errado no seguinte trecho de código? int vetorAmostra[10]; for (int indice
= 1; indice <= 10; indice++) vetorAmostra[indice] = 3*indice;
8. Suponha que esperemos que os elementos do vetor sejam ordenados de forma que a[0]
≤ a[1] ≤ a[2] ≤
...
Entretanto, para termos certeza, queremos que nosso programa teste o vetor e envie um aviso caso se descubra que alguns elementos estão fora de ordem. O código seguinte deveria enviar esse aviso, mas contém um erro. Qual é? double a[10];
for (int indice = 0; indice < 10; indice++) if (a[indice] > a[indice + 1]) cout << "Os elementos do vetor " << indice << " e " << (indice + 1) << " estão fora de ordem.";
9. Escreva um código em C++ que preencha um vetor com 20 valores de tipo int lidos a partir do teclado. Não precisa escrever um programa inteiro, apenas o código para isso, mas forneça as declarações do vetor e de todas as variáveis. 10. Suponha que você tenha a seguinte declaração de vetor em seu programa: int seuVetor[7];
Suponha também que, em sua implementação do C++, variáveis do tipo int utilizem dois bytes de memória. Quando seu programa for executado, quanta memória esse vetor consumirá? Suponha que, quando você executar o programa, o sistema atribua o endereço de memória 1000 à variável indexada seuVetor[0]. Qual será o endereço da variável indexada seuVetor[3]?
5.2
Vetores em Funções
Você pode utilizar tanto variáveis indexadas de vetor quanto vetores completos como argumentos de funções. Vamos tratar primeiro das variáveis indexadas de vetor como argumentos de funções. ■
VARIÁVEIS INDEXADAS COMO ARGUMENTOS DE FUNÇÃO
Uma variável indexada pode ser um argumento de uma função exatamente da mesma forma que qualquer variável do tipo-base de vetor pode ser um argumento. Por exemplo, suponha que um programa contenha as seguintes declarações: double i,
n, a[10];
Se minhaFuncao requer um argumento de tipo double, então a linha seguinte é legal: minhaFuncao(n);
Como uma variável indexada do vetor a também é uma variável de tipo double, exatamente como n, a linha seguinte também é legal: minhaFuncao(a[3]);
Uma variável indexada pode ser um argumento chamado por valor ou por referência. Há, contudo, uma sutileza que se aplica a variáveis indexadas utilizadas como argumentos. Por exemplo, considere a seguinte chamada de função:
124
Vetores
minhaFuncao(a[i]);
Se o valor de i é 3, então o argumento é a[3]. Por outro lado, se o valor de i é 0, essa chamada é equivalente à seguinte: minhaFuncao(a[0]);
A expressão indexada é avaliada a fim de determinar exatamente que variável indexada é fornecida como argumento.
11. Considere a seguinte definição de função: void triplicador(int& n) { n = 3*n; } Qual das seguintes chamadas de função é aceitável? int a[3] = {4, 5, 6}, numero = 2; triplicador(a[2]); triplicador(a[3]); triplicador(a[numero]); triplicador(a); triplicador(numero); 12. O que há de errado (se houver algo) com o seguinte código? A definição de triplicador é dada no Exercício de Autoteste 11. int b[5] = {1, 2, 3, 4, 5}; for (int i = 1; i <= 5; i++) triplicador(b[i]);
■
VETORES INTEIROS COMO ARGUMENTOS DE FUNÇÃO
Uma função pode ter um parâmetro formal para um vetor completo de modo que, quando a função é chamada, o argumento conectado a esse parâmetro formal seja um vetor completo. Entretanto, um parâmetro formal de um vetor completo não é um parâmetro chamado por valor nem por referência, é um novo tipo de parâmetro formal que se chama parâmetro vetorial. Vamos começar com um exemplo. A função definida no Painel 5.3 possui um parâmetro vetorial, a, que será substituído por um vetor completo quando a função for chamada. Possui também um parâmetro comum chamado por valor ( tamanho) que se presume ser um valor inteiro igual ao tamanho do vetor. A função preenche seu argumento de vetor (ou seja, preenche todas as variáveis indexadas do vetor) com valores digitados no teclado; então, a função envia uma mensagem para a tela com o índice do último índice de vetor usado.
Painel 5.3
Função com um parâmetro vetorial
DECLARAÇÃO DE FUNÇÃO void preenche(int a[], int
tamanho); //Pré-condição: tamanho é o tamanho declarado do vetor a. //O usuário digitará os inteiros da variável tamanho. //Pós-condição: O vetor a é preenchido com inteiros da variável tamanho //a partir do teclado.
DEFINIÇÃO DE FUNÇÃO void preenche(int a[], int tamanho);
{ cout << "Informe " << tamanho << " os números:\n"; for (int i = 0; i < tamanho; i++) cin >> a[i]; cout << "O último índice de vetor usado é " << (tamanho - 1) << endl; }
Vetores em Funções
125
O parâmetro formal int a[] é um parâmetro vetorial. Os colchetes, sem nenhuma expressão de índice dentro, são usados pelo C++ para indicar um parâmetro vetorial. Um parâmetro vetorial não é exatamente um parâmetro chamado por referência, mas, para a maioria dos objetivos práticos, se comporta de maneira bastante similar a um parâmetro chamado por referência. Vamos a um exemplo detalhado para ver como um argumento vetorial funciona nesse caso. (Um argumento vetorial é, obviamente, um vetor conectado a um parâmetro vetorial, como a[].) Quando a função preenche é chamada, deve ter dois argumentos: o primeiro fornece um vetor de inteiros e o segundo deve fornecer o tamanho declarado do vetor. Por exemplo, a chamada de função seguinte é aceitável: int nota[5],
numeroDeNotas = 5; preenche(nota, numeroDeNotas);
Esta chamada a preenche preencherá o vetor nota com cinco inteiros digitados ao teclado. Observe que o parâmetro formal a[] (que é utilizado na declaração de função e no cabeçalho da definição de função) é dado com os colchetes, mas sem expressão de índice. (Você pode inserir um número dentro dos colchetes para um parâmetro vetorial, mas o compilador simplesmente ignorará esse número, por isso neste livro não usaremos tais números.) Por outro lado, o argumento dado na chamada de função (nota, nesse exemplo) é dado sem colchetes ou expressão de índice. O que acontece com o argumento vetorial nota nesta chamada de função? Falando de modo geral, o argumento nota é conectado ao parâmetro vetorial formal a no corpo da função, e então o corpo da função é executado. Assim, a chamada de função preenche(nota, numeroDeNotas);
é equivalente ao seguinte código: 5 é o valor de numeroDeNotas
{
tamanho = 5; cout << "Digite " << size << " os números:\n"; for (int i = 0; i < size; i++) cin >> nota[i]; cout << "O último índice de vetor usado é " << (tamanho - 1) << endl; }
O parâmetro formal a é um tipo de parâmetro diferente dos que vimos até agora. O parâmetro formal a é apenas um "guardador" de lugar para o argumento nota. Quando a função preenche é chamada com nota como argumento vetorial, o computador se comporta como se a fosse substituído pelo argumento correspondente nota. Quando um vetor é utilizado como um argumento em uma chamada de função, qualquer ação executada no parâmetro vetorial é executada sobre o argumento vetorial, portanto os valores das variáveis indexadas do argumento vetorial podem ser alterados pela função. Se o parâmetro formal no corpo da função é alterado (por exemplo, com um comando cin), o argumento vetorial será alterado. Até agora, talvez pensemos que um parâmetro vetorial é apenas um parâmetro chamado por referência para um vetor. Isso é quase verdade, mas um parâmetro vetorial é ligeiramente diferente de um parâmetro chamado por referência. Para ajudar a explicar a diferença, vejamos alguns detalhes sobre vetores. Lembre-se de que um vetor é armazenado como um bloco de memória contíguo. Por exemplo, considere a seguinte declaração do vetor nota: int nota[5];
Quando se declara esse vetor, o computador reserva memória suficiente para abrigar cinco variáveis de tipo int, que são armazenadas uma após a outra na memória do computador. O computador não se lembra dos endereços de cada uma das cinco variáveis indexadas; lembra-se apenas do endereço da variável indexada nota[0]. O computador também se lembra de que nota possui um total de cinco variáveis indexadas, todas de tipo int. Não se lembra do endereço na memória de qualquer variável indexada além de nota[0]. Por exemplo, quando seu programa precisa de nota[3], o computador calcula o endereço de nota[3] a partir do endereço de nota[0]. Assim, para obter o endereço de nota[3], o computador toma o endereço de nota[0] e acrescenta um número que representa a quantidade de memória utilizada por três variáveis int; o resultado é o endereço de nota[3]. Visto dessa forma, um vetor possui três partes: o endereço (localização na memória) da primeira variável indexada, o tipo-base do vetor (que determina quanta memória cada variável indexada utiliza) e o tamanho do vetor
126
Vetores
(ou seja, o número de variáveis indexadas). Quando um vetor é utilizado como um argumento vetorial de uma função, apenas a primeira dessas três partes é dada para a função. Quando um argumento vetorial é conectado com seu parâmetro formal correspondente, tudo o que é conectado é o endereço da primeira variável indexada do vetor. O tipo-base do argumento vetorial deve ser idêntico ao tipo-base do parâmetro formal, portanto a função sabe também o tipo-base do vetor. Entretanto, o argumento vetorial não diz à função o tamanho do vetor. Quando o código no corpo da função é executado, o computador sabe onde o vetor começa na memória e quanta memória cada variável indexada usa, mas (a não ser que você tome providências especiais) não sabe quantas variáveis indexadas o vetor possui. Por isso é tão importante que você sempre tenha outro argumento int dizendo à função o tamanho do vetor. (É por isso também que um parâmetro vetorial não é igual a um parâmetro chamado por referência. Pode-se pensar em um parâmetro vetorial como uma forma fraca de um parâmetro chamado por referência em que tudo sobre o vetor é dito à função, exceto o tamanho do vetor.) 2 Esses parâmetros vetoriais podem parecer um tanto estranhos, mas possuem pelo menos uma boa propriedade como resultado direto de sua definição aparentemente estranha. Essa vantagem será mais bem ilustrada se olharmos para o nosso exemplo da função preenche, dado no Painel 5.3. Essa mesma função pode ser usada para preencher um vetor de qualquer ta- manho , desde que o tipo-base do vetor seja int. Por exemplo, suponha que você tenha as seguintes declarações de vetor: int
nota[5], tempo[10];
A primeira das seguintes chamadas de che o vetor tempo com dez valores:
preenche completa
o vetor
nota com
cinco valores, e a segunda preen-
preenche(nota, 5); preenche(tempo, 10);
Você pode usar a mesma função para argumentos vetoriais de diferentes tamanhos, porque o tamanho é um argumento separado. ■
PARÂMETRO MODIFICADOR
const
Quando você usa um argumento vetorial em uma chamada de função, a função pode alterar os valores armazenados no vetor. Isso normalmente é bom. Entretanto, em uma definição de função complicada, você pode querer escrever um código que altere inadvertidamente um ou mais valores armazenados em um vetor, apesar de o vetor não dever ser alterado. Como precaução, você pode dizer ao compilador que não pretende alterar o argumento do vetor, e o computador verificará que seu código não altere inadvertidamente qualquer dos valores no vetor. Para dizer ao compilador que um argumento vetorial não deve ser alterado pela sua função, insira o modificador const antes do parâmetro vetorial para aquela posição de argumento. Um parâmetro vetorial que é modificado por um const é chamado de parâmetro vetorial constante . PARÂMETROS FORMAIS E ARGUMENTOS VETORIAIS
Um argumento de uma função pode ser um vetor completo, mas um argumento para um vetor completo não é um argumento chamado por valor nem um argumento chamado por referência. É um novo tipo de argumento conhecido como argumento vetorial. Quando um argumento vetorial é conectado a um parâmetro vetorial, tudo o que é fornecido para a função é o endereço na memória da primeira variável indexada do argumento vetorial (aquele indexado por 0). O argumento vetorial não diz à função o tamanho do vetor. Portanto, quando se tem um parâmetro vetorial de uma função, normalmente é preciso ter outro parâmetro formal de tipo int que forneça o tamanho do vetor (como no exemplo abaixo). Um argumento vetorial é como um argumento chamado por referência da seguinte forma: se o corpo da função altera o parâmetro vetorial, então, quando a função é chamada, essa alteração é, na verdade, feita no argumento vetorial. Assim, uma função pode alterar os valores de um argumento vetorial (ou seja, pode mudar os valores de suas variáveis indexadas). A sintaxe para uma declaração de função com um parâmetro vetorial é a seguinte: SINTAXE Tipo_Retornado Nome_Da_Funcao(..., Tipo_Base Nome_Do_Vetor[],...);
EXEMPLO void somaVetor(double&
2.
soma,
double a[], int tamanho);
Se você já ouviu falar em ponteiros, isso soará como se fossem ponteiros e, com efeito, um argumento vetorial é transmitido passando-se um ponteiro para sua primeira variável indexada (a de número 0). Trataremos disso no Capítulo 10. Se você nunca ouviu falar de ponteiros, pode ignorar esta nota.
Vetores em Funções
127
Por exemplo, a seguinte função apresenta como saída os valores em um vetor, mas não altera os valores no vetor: void mostreAoMundo(int a[], int tamanhoDea)
//Pré-condição: tamanhoDea é o tamanho declarado do vetor a. //Todas as variáveis indexadas de a receberam valores. //Pós-condição: Os valores em a foram escritos na tela. { cout << " O vetor contém os seguintes valores:\n"; for (int i = 0; i < tamanhoDea; i++) cout << a[i] << " "; cout << endl; }
Essa função trabalhará bem. Entretanto, como medida de segurança adicional, você pode acrescentar o modificador const ao cabeçalho da função, como se segue: void mostreAoMundo(const int a[], int tamanhoDea)
Com o acréscimo desse modificador const, o computador emitirá uma mensagem de erro se sua definição de função contiver um erro que altere qualquer dos valores no argumento vetorial. Por exemplo, a seguinte versão da função mostreAoMundo contém um erro que altera inadvertidamente o valor do argumento vetorial. Felizmente, esta versão da definição de função inclui o modificador const; assim, uma mensagem de erro nos dirá que o vetor foi alterado. Essa mensagem de erro ajudará a explicar o erro: void mostreAoMundo(const int a[], int tamanhoDea)
//Pré-condição: tamanhoDea é o tamanho declarado do vetor a. //Todas as variáveis indexadas de a receberam valores. //Pós-condição: Os valores em a foram escritos na tela. { cout << " O vetor contém os seguintes valores:\n"; for (int i = 0; i < tamanhoDea; a[i]++) Erro, mas o compilador não o cout << a[i] << " "; acusará a não ser que você cout << endl; utilize o modificador const. }
Se não houvéssemos usado o modificador const na definição de função acima e tivéssemos cometido o erro mostrado, a função compilaria e seria executada sem mensagens de erro. Entretanto, o código conteria um loop infinito que incrementaria continuamente a[0] e escreveria um novo valor na tela. O problema com esta versão incorreta de mostreAoMundo é que o item errado é incrementado no loop for. A variável indexada a[i] é incrementada, mas o item índice i é que deveria ser incrementado. Nesta versão incorreta, o índice i começa com o valor 0 e esse valor nunca é alterado. Mas a[i], que é o mesmo que a[0], é incrementada. Quando a variável indexada a[i] é incrementada, altera-se o valor no vetor e, como incluímos o modificador const, o computador enviará uma mensagem de aviso. Essa mensagem de erro servirá como uma pista do que está errado. Normalmente há uma declaração de função em seu programa, além da definição de função. Quando se usa o modificador const em uma definição de função, deve-se também usá-lo na declaração de função, de modo que o cabeçalho da função e a declaração de função sejam consistentes. O modificador const pode ser usado com qualquer tipo de parâmetro, mas normalmente é usado apenas com parâmetros vetoriais e parâmetros chamados por referência para classes, das quais trataremos nos Capítulos 6 e 7. USO INCONSISTENTE DE PARÂMETROS DE
const
O modificador de parâmetro const é uma proposição de tudo ou nada. Se você usá-lo para um parâmetro vetorial de tipo particular, deve usá-lo para todos os outros parâmetros vetoriais que possuam esse tipo e que não sejam alterados pela função. O motivo para isso tem a ver com chamadas de função dentro de chamadas de função. Considere a definição da função mostraDiferenca, que é dada a seguir com a declaração de uma função usada na definição: double calculaMedia(int a[], int numeroUsado); //Retorna a média dos n primeiros elementos do vetor a(n é o valor passado por
128
Vetores
// numeroUsado. O vetor a não é alterado. void mostraDiferenca(const int a[], int numeroUsado) { double media = calculaMedia(a, numeroUsado); cout << "A média dos " << numeroUsado << " números = " << media << endl << "Os números são:\n"; for (int indice = 0; indice < numeroUsado; indice++) cout << a[indice] << " difere da média por " << (a[indice] - media) << endl; } O código acima emitirá uma mensagem de erro ou de aviso na maioria dos compiladores. A função calculaMedia não altera seu parâmetro a. Entretanto, quando o compilador processa a definição de função para mostraDiferenca, ele pensará que calculaMedia altera (ou, pelo menos, poderia alterar) o valor de seu parâmetro a. Isso porque, quando ele traduz a definição de função para mostraDiferenca, tudo o que o compilador sabe a respeito da função calculaMedia é a declaração de função de calculaMedia, que não contém um const para dizer ao compilador que o parâmetro a não será alterado. Assim, se você usa const com o parâmetro a na função mostraDiferenca, deve usar o modificador const também com o parâmetro a na função calculaMedia. A declaração de função para calculaMedia deve ser a seguinte: double calculaMedia(const int a[], int numeroUsado);
■
FUNÇÕES QUE RETORNAM UM VETOR
Uma função pode não retornar um vetor da mesma forma que retorna um valor de tipo int ou double. Não há como se obter algo mais ou menos equivalente para uma função que retorna um vetor. O que se deve fazer é retornar um ponteiro para o vetor. Abordaremos esse tópico quando discutirmos a interação de vetores e ponteiros no Capítulo 10. Até que você aprenda o que são ponteiros, não há como escrever uma função que retorne um vetor.
GRÁFICO DE PRODUÇÃO O Painel 5.4 contém um programa que utiliza um vetor e alguns parâmetros vetoriais. Esse programa para a Companhia Clímax de Fabricação de Colheres de Plástico apresenta um gráfico de barras exibindo a produti vidade de cada uma de suas quatro fábricas em uma dada semana. As fábricas mantêm cifras de produção separadas para cada departamento, como o departamento de colheres de chá, departamento de colheres de sopa, departamento de colheres de coquetel simples, departamento de colheres de coquetel coloridas, e assim por diante. Além disso, cada uma das quatro fábricas possui um número diferente de departamentos. Como você pode ver pelo diálogo programa-usuário no Painel 5.4, o gráfico utiliza um asterisco para cada 100 unidades de produção. Como a saída é em unidades de 1000, deve ser colocada em escala sendo dividida por 1000. Isso representa um problema, porque o computador precisa exibir um número inteiro de asteriscos. Não pode exibir 1,6 asteriscos para 1600 unidades. Por isso, fazemos um arredondamento para o milhar mais próximo. Assim, 1600 será o mesmo que 2000 e se transformará em dois asteriscos. O vetor producao contém a produção total para cada uma das quatro fábricas. Em C++, os índices de vetor sempre começam no 0. Mas como as fábricas são numeradas de 1 a 4 e não de 0 a 3, colocamos a produção total para a fábrica número n na variável indexada producao[n - 1]. A saída total para a fábrica número 1 estará contida na producao[0], as cifras para a fábrica 2 estarão contidas na producao[1], e assim por diante. Como a saída é em milhares de unidades, o programa colocará em escala os valores dos elementos do vetor. Se a saída total da fábrica número 3 é 4040 unidades, o valor de producao[2] será inicialmente fixado como 4040. Esse valor de 4040 será, então, escalado como 4, de forma que o valor de producao[2] seja alterado para 4 e quatro asteriscos sejam apresentados para representar a saída da fábrica número 3. Essa operação é realizada pela função escala, que toma todo o vetor producao como um argumento e altera os valores armazenados no vetor. A função arredonda efetua o arredondamento do seu argumento para o inteiro mais próximo. Por exemplo, arredonda(2.3) apresenta como resultado 2, e arredonda(2.6) apresenta como resultado 3. A função arredonda foi discutida no Capítulo 3, no exemplo de programação intitulado "Uma Função Arredondadora".
Vetores em Funções Painel 5.4
1 2 3 4 5
Programa gráfico de produção ( parte 1 de 3) //Lê dados e exibe um gráfico de barras mostrando a produtividade de cada fábrica. #include #include using namespace std; const int NUMBER_OF_PLANTS = 4;
6 void inputData( int a[], int lastPlantNumber); 7 //Pré-condição: lastPlantNumber é o tamanho declarado do vetor a. 8 //Pós-condição: Para plantNumber = 1 até lastPlantNumber: 9 //a[plantNumber-1] é igual à produção total da fábrica de número plantNumber. 10 void scale(int a[], int size ); 11 //Pré-condição: a[0] até a[size-1] tem todos valor não-negativo. 12 //Pós-condição: a[i] foi alterado para o número de milhares (arredondado para 13 //um inteiro) que estava originalmente em a[i], para todo i, tal que 0 <= i <= size-1. 14 void graph( const int asteriskCount[], int lastPlantNumber); 15 //Pré-condição: a[0] até a[lastPlantNumber-1] tem todos valor não-negativo. 16 //Pós-condição: Um gráfico de barras foi apresentado dizendo que a fábrica 17 //número N produziu a[N-1] milhares de unidades para cada N, tal que 18 //1 <= N <= lastPlantNumber 19 void getTotal(int& sum); 20 //Lê inteiros não-negativos a partir do teclado e 21 //coloca o total em sum. 22 int round(double number); 23 //Pré-condição: number >= 0. 24 //Retorna número arredondado para o inteiro mais próximo. 25 void printAsterisks(int n); 26 //Imprime n asteriscos na tela. 27 int main( ) 28 { 29 int production[NUMBER_OF_PLANTS]; 30 31
cout << "Este programa apresenta um gráfico mostrando\n" << "a produção de cada fábrica na companhia.\n";
32 33 34 35 36 }
inputData(production, NUMBER_OF_PLANTS); scale(production, NUMBER_OF_PLANTS); graph(production, NUMBER_OF_PLANTS); return 0;
37 void inputData(int a[], int lastPlantNumber) 38 { 39 for (int plantNumber = 1; 40 plantNumber <= lastPlantNumber; plantNumber++) 41 { 42 cout << endl 43 << "Informe os dados de produção para a fábrica número " 44 << plantNumber << endl; 45 getTotal(a[plantNumber - 1]); 46 } 47 } 48 void getTotal(int& sum)
129
130
Vetores
Painel 5.4
Programa gráfico de produção ( parte 2 de 3)
49 { 50 cout << "Informe o número de unidades produzidas por cada departamento.\n" 51 << "Inclua um número negativo ao final da lista.\n"; 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
sum = 0; int next; cin >> next; while (next >= 0) { sum = sum + next; cin >> next; } cout << "Total = " << sum << endl; } void scale(int a[], int size) { for (int index = 0; index < size; index++) a[index] = round(a[index]/1000.0); }
68 int round(double number) 69 { return static_cast(floor(number + 0.5)); 70 71 } 72 void graph(const int asteriskCount[], int lastPlantNumber) 73 { 74 cout << "Unidades produzidas em milhares de unidades:\n"; for (int plantNumber = 1; 75 76 plantNumber <= lastPlantNumber; plantNumber++) 77 { 78 cout << "Fábrica #" << plantNumber << " "; 79 printAsterisks(asteriskCount[plantNumber - 1]); 80 cout << endl; 81 } 82 } 83 void printAsterisks(int n) 84 { for (int count = 1; count <= n; count++) 85 86 cout << "*"; 87 }
DIÁLOGO PROGRAMA-USUÁRIO Este programa apresenta um gráfico mostrando a produção de cada fábrica na companhia. Informe os dados de produção para a fábrica número 1 Informe o número de unidades produzidas por cada departamento. Inclua um número negativo ao final da lista. 2000 3000 1000 -1 Total = 6000 Informe os dados de produção para a fábrica número 2 Informe o número de unidades produzidas por cada departamento. Inclua um número negativo ao final da lista. 2050 3002 1300 -1 Total = 6352
Vetores em Funções Painel 5.4
131
Programa gráfico de produção ( parte 3 de 3)
Informe os dados de produção para a fábrica número 3
Informe o número de unidades produzidas por cada departamento. Inclua um número negativo ao final da lista. 5000 4020 500 4348 -1
Total = 13868 Informe os dados de produção para a fábrica número 4 Informe o número de unidades produzidas por cada departamento. Inclua um número negativo ao final da lista. 2507 6050 1809
-1
Total = 10366 Unidades produzidas em milhares de unidades: Fábrica Fábrica Fábrica Fábrica
#1 #2 #3 #4
****** ****** ************** **********
13. Escreva uma definição de função para uma função chamada maisUm, que possui um parâmetro formal para um vetor de inteiros e aumenta o valor de cada elemento do vetor em 1. Acrescente quaisquer outros parâmetros formais que sejam necessários. 14. Considere a seguinte definição de função: void tambem2(int a[], int quantos) { for (int indice = 0; indice < quantos; indice++) a[indice] = 2; } Qual das seguintes seria uma chamada de função aceitável? int meuVetor[29]; tambem2(meuVetor, 29); tambem2(meuVetor, 10); tambem2(meuVetor, 55); "Ei tambem2. Por favor, venha aqui." int seuVetor[100]; tambem2(seuVetor, 100); tambem2(seuVetor[3], 29); 15. Insira const antes de qualquer dos seguintes parâmetros vetoriais que possam ser alterados para parâmetros vetoriais constantes. void saida(double a[], int tamanho); //Pré-condição: a[0] até a[tamanho - 1] possuem valores. //Pós-condição: a[0] até a[tamanho - 1] foram escritos. void descartaImpar(int a[], int tamanho);
//Pré-condição: a[0] até a[tamanho - 1] possuem valores. //Pós-condição: Todos os números ímpares em a[0] até a[tamanho - 1] //foram alterados para 0. 16. Escreva uma função chamada foraDeOrdem que tome como parâmetros um vetor de double e um parâmetro int chamado tamanho e retorne um valor de tipo int. Essa função testará esse vetor para ver se está fora de ordem, o que significa que o vetor viola a seguinte condição: a[0] <= a[1] <= a[2] <= ... A função apresenta como saída -1 se os elementos não estão fora de ordem; caso contrário, retornará o índice do primeiro elemento do vetor que esteja fora de ordem. Por exemplo, considere a declaração
132
Vetores
a[10] = {1.2, 2.1, 3.3, 2.5, 4.5, 7.9, 5.4, 8.7, 9.9, 1.0}; No vetor acima, a[2] e a[3] são o primeiro par fora de ordem, e a[3], é o primeiro elemento fora de ordem, então a função apresenta como saída 3. Se o vetor fosse colocado em ordem, a função apresentaria como saída -1. double
5.3
Programando com Vetores Nunca confie em impressões gerais, meu rapaz. Concentre-se nos detalhes. Sir Arthur Conan Doyle, Um Caso de Identidade (Sherlock Holmes)
Esta seção discute vetores parcialmente preenchidos e fornece uma breve introdução à ordenação de vetores e à busca em vetores. Esta seção não inclui novas informações sobre a linguagem C++, mas acrescenta mais exemplos práticos com parâmetros vetoriais em C++. ■ VETORES PARCIALMENTE PREENCHIDOS
Muitas vezes o tamanho exato necessário para um vetor não é conhecido quando um programa é escrito, ou o tamanho pode variar de uma execução do programa para outra. Uma forma comum e fácil de se lidar nesta situação é declarar o vetor com o maior tamanho que o programa poderia necessitar. O programa é, então, livre para usar o máximo ou o mínimo do vetor de que necessitar. Vetores parcialmente preenchidos requerem algum cuidado. O programa precisa controlar quanto do vetor foi usado e não deve referenciar nenhuma variável indexada que não tenha recebido um valor. O programa no Painel 5.5 ilustra essa questão. O programa lê uma lista de pontuações de golfe e mostra quanto cada pontuação difere da média. Esse programa trabalhará com listas de uma até dez pontuações, e de qualquer comprimento entre esses dois extremos. As pontuações são armazenadas no vetor pontuacao, que possui dez variáveis indexadas, mas o programa utiliza apenas a parte do vetor de que necessita. A variável numeroUsado controla quantos elementos estão armazenados no vetor. Os elementos (ou seja, as pontuações) são armazenados nas posições pontuacao[0] até pontuacao[numeroUsado - 1]. Os detalhes são bastante similares aos que seriam se numeroUsado fosse o tamanho declarado do vetor e o vetor completo fosse usado. Em particular, a variável numeroUsado normalmente precisa ser um argumento para qualquer função que manipule o vetor parcialmente preenchido. Como o argumento numeroUsado (quando usado adequadamente) pode muitas vezes assegurar que a função não referenciará um índice de vetor ilegal, isso às vezes (mas não sempre) elimina a necessidade de um argumento que forneça o tamanho declarado do vetor. Por exemplo, as funções mostraDiferenca e calculaMedia utilizam o argumento numeroUsado para assegurar que apenas índices de vetor legais sejam usados. Entretanto, a função preencheVetor precisa saber o tamanho máximo declarado para o vetor de modo que não ultrapasse a capacidade deste. NÃO POUPE PARÂMETROS FORMAIS Observe a função preencheVetor no Painel 5.5. Quando preencheVetor é chamada, o tamanho declarado do vetor MAX_NUMERO_PONTUACAO é fornecido como um dos argumentos, como exibido na seguinte chamada de função do Painel 5.5: preencheVetor(pontuacao, MAX_NUMERO_PONTUACAO, numeroUsado); Você pode protestar dizendo que MAX_NUMERO_PONTUACAO é uma constante definida globalmente, e, assim, poderia ser usada na definição de preencheVetor sem a necessidade de ser transformada em argumento. Você teria razão, e se não usássemos preencheVetor em nenhum programa além do exibido no Painel 5.5, poderíamos deixar de incluir MAX_NUMERO_PONTUACAO como um argumento de preencheVetor . Entretanto, preencheVetor é uma função de uso geral que você pode querer utilizar em vários programas diferentes. Com efeito, utilizamos também a função preencheVetor no programa do Painel 5.6, discutido na próxima subseção. No programa do Painel 5.6, o argumento para o tamanho declarado do vetor é uma constante global nomeada diferente. Se tivéssemos escrito a constante global MAX_NUMERO_PONTUACAO no corpo da função preencheVetor , não poderíamos reutilizar a função no programa do Painel 5.6.
Programando com Vetores
133
Mesmo que utilizássemos preencheVetor em apenas um programa, ainda seria uma boa idéia transformar o tamanho declarado do vetor em um argumento de preencheVetor . Exibir o tamanho declarado do vetor como um argumento nos lembra de que a função necessita dessa informação de maneira fundamental.
Painel 5.5
1 2 3 4
Vetor parcialmente preenchido ( parte 1 de 2) //Mostra a diferença entre cada entrada em uma lista de pontuações de golfe e sua média. #include using namespace std; const int MAX_NUMBER_SCORES = 10;
5 void fillArray(int a[], int size, int& numberUsed); 6 //Pré-condição: size é o tamanho declarado do vetor a. 7 //Pós-condição: numberUsed é o número de valores armazenado em a. 8 //a[0] até a[numberUsed-1] foi preenchido com 9 //inteiros não-negativos lidos a partir do teclado. 10 double computeAverage(const int a[], int numberUsed); 11 //Pré-condição: a[0] até a[numberUsed-1] tem valores; numberUsed > 0. 12 //Retorna a média dos números a[0] até a[numberUsed-1]. 13 void showDifference(const int a[], int numberUsed); 14 //Pré-condição: As primeiras variáveis indexadas numberUsed de a possuem valores. 15 //Pós-condição: Mostra na tela em quanto os primeiros 16 //numberUsed elementos do vetor a diferem de sua média. 17 int main( ) 18 { 19 int score[MAX_NUMBER_SCORES], numberUsed; 20 21
cout << "Este programa lê pontuações de golfe e mostra\n" << "quanto cada uma difere da média.\n";
22 23 24
cout << "Informe as pontuações de golfe:\n"; fillArray(score, MAX_NUMBER_SCORES,numberUsed); showDifference(score, numberUsed);
25 26 }
return 0;
27 void fillArray(int a[], int size, int& numberUsed) 28 { 29 cout << "Forneça até " << size << " números não-negativos.\n" 30 << "Assinale o final da lista com um número negativo.\n"; 31 int next, index = 0; 32 cin >> next; 33 while ((next >= 0) && (index < size)) 34 { 35 a[index] = next; 36 index++; 37 cin >> next; 38 } 39 40 }
numberUsed = index;
41 double computeAverage(const int a[], int numberUsed) 42 { 43 double total = 0; 44 for (int index = 0; index < numberUsed; index++) 45 total = total + a[index];
134
Vetores
Painel 5.5
46 47 48 49 50 51 52 53 54 55 56 }
Vetor parcialmente preenchido ( parte 2 de 2)
if (numberUsed
> 0)
{ return (total/numberUsed);
} else
{ cout << "ERRO: número de elementos é 0 em computeAverage.\n" << "computeAverage retorna 0.\n"; return 0; }
57 void showDifference(const int a[], int numberUsed) 58 { double average = computeAverage(a, numberUsed); 59 60 cout << "Média das " << numberUsed 61 << " pontuações = " << average << endl 62 << "As pontuações são:\n"; for (int index = 0; index < numberUsed; index++) 63 64 cout << a[index] << " diferem da média por " 65 << (a[index] - average) << endl; 66 }
DIÁLOGO PROGRAMA-USUÁRIO Este programa lê pontuações de golfe e mostra quanto cada uma difere da média. Informe as pontuações de golfe: Forneça até 10 números não-negativos. Assinale o final da lista com um número negativo. 69 74 68 -1
A média das 3 pontuações = 70.3333 As pontuações: 69 difere da média por -1.33333 74 difere da média por 3.66667 68 difere da média por -2.33333
BUSCAS EM VETOR Uma tarefa comum de programação é buscar um determinado valor em um vetor. Por exemplo, o vetor pode conter os números de identificação escolar de todos os estudantes de um determinado curso. Para dizer se um estudante em particular está matriculado, efetua-se uma busca no vetor para verificar se este contém o número do estudante. O programa simples no Painel 5.6 preenche um vetor e depois procura neste os valores especificados pelo usuário. Um programa de aplicação real seria bem mais elaborado, mas este mostra tudo o que é essencial em um algoritmo de busca seqüencial. A busca seqüencial é o algoritmo de busca mais simples que se possa imaginar. O programa procura pelos elementos do vetor em ordem, do primeiro ao último, para ver se o número procurado é igual a algum dos elementos do vetor. No Painel 5.6 a função busca é utilizada para efetuar a busca no vetor. Quando se efetua uma busca em um vetor, muitas vezes se deseja saber mais do que apenas se o valor procurado está ou não no vetor. Se o valor procurado está no vetor, em geral se quer saber o índice da variável indexada que abriga o valor procurado, já que o índice pode servir como guia para alguma informação adicional sobre o valor procurado. Desta forma, projetamos a função busca para retornar um índice dando a localização no vetor do valor procurado, desde que o valor procurado esteja, de fato, no vetor. Se o valor procurado não estiver no vetor, busca apresenta como saída -1. Vamos estudar a função busca com mais atenção. A função busca utiliza um loop while para verificar os elementos do vetor um após o outro a fim de verificar se algum deles é igual ao valor procurado. A variável encontrado é utilizada como uma sinalização para registrar se o elemento procurado foi ou não encontrado. Se o elemento procurado foi encontrado no vetor, encontrado é true, o que encerra o loop while.
Programando com Vetores Painel 5.6
1 2 3 4 5 6 7 8 9 10 11 12 13 14
Efetuando uma busca em um vetor ( parte 1 de 2) //Efetua uma busca em um vetor parcialmente preenchido de inteiros não-negativos. #include using namespace std; const int DECLARED_SIZE = 20;
void fillArray(int a[], int size, int& numberUsed);
//Pré-condição: size é o tamanho declarado do vetor a. //Pós-condição: numberUsed é o número de valores armazenado em a. //a[0] até a[numberUsed-1] foi preenchido com //inteiros não-negativos a partir do teclado. int search(const int a[], int numberUsed, int target); //Pré-condição: numberUsed é <= ao tamanho declarado de a. //Além disso, a[0] até a[numberUsed-1] possuem valores. //Retorna o primeiro índice tal que a[index] == target, //desde que exista tal índice; caso contrário, retorna -1.
15 int main( ) 16 { 17 int arr[DECLARED_SIZE], listSize, target; 18
fillArray(arr, DECLARED_SIZE, listSize);
19 20 21 22 23 24
char ans; int result; do
25 26 27 28 29 30 31 32 33 34 35 36 37 }
{ cout << "Informe um número para ser procurado: "; cin >> target; result = search(arr, listSize, target); if (result == -1)
cout << target << " não está na lista.\n"; else
cout << target << " está armazenado na posição do vetor " << result << endl << "(Lembre-se: a primeira posição é 0.)\n"; cout << "Outra busca?(s/n mais tecla Enter): "; cin >> ans; } while ((ans != ’n’) && (ans != ’N’)); cout << "Fim do programa.\n"; return 0;
38 void fillArray(int a[], int size, int& numberUsed) 39 40 int search(const int a[], int numberUsed, int target) 41 { 42 int index = 0; 43 bool found = false; 44 while ((!found) && (index < numberUsed)) 45 if (target == a[index]) 46 found = true; 47 else 48 index++; 49 50
if (found) return index;
135
136
Vetores
Painel 5.6
51 52 53 }
Efetuando uma busca em um vetor ( parte 2 de 2)
else return -1;
DIÁLOGO PROGRAMA-USUÁRIO Forneça até 20 números inteiros não-negativos. Assinale o final da lista com um número negativo. 10 20 30 40 50 60 70 80 -1
Informe um número para ser procurado: 10 10 está armazenado na posição de vetor 0 (Lembre-se: a primeira posição é 0.) Outra busca? (s/n mais tecla Enter): s Informe um número para ser procurado: 40 40 está armazenado na posição de vetor 3 (Lembre-se: a primeira posição é 0.) Outra busca? (s/n mais tecla Enter): s Informe um número para ser procurado: 42 42 não está na lista. Outra busca? (s/n mais tecla Enter): n Final do programa.
ORDENANDO UM VETOR
Uma das tarefas de programação mais comuns e certamente a mais estudada é a ordenação de uma lista de valores, como uma lista de cifras de venda que deve ser ordenada do menor para o maior ou do maior para o menor, ou uma lista de palavras que deve ser colocada em ordem alfabética. Este exemplo descreve uma função chamada ordena que ordenará um vetor de números parcialmente preenchido do menor para o maior. O procedimento ordena possui um parâmetro vetorial, a. O vetor a será parcialmente preenchido, portanto existe um parâmetro formal adicional chamado numeroUsado que diz quantas posições de vetor são utilizadas. Assim, a declaração e pré-condição da função ordena são as seguintes: void ordena(int a[], int numeroUsado);
//Pré-condição: numeroUsado <= tamanho declarado do vetor a. //Os elementos do vetor de a[0] até a[numeroUsado - 1] possuem valores. A função ordena rearranja os elementos no vetor a de modo que, depois que a
pletada, os elementos são ordenados da seguinte forma: a[0]
≤ a[1] ≤ a[2] ≤
...
≤
chamada de função é com-
a[numeroUsado - 1] ordenar é chamado ordenação por seleção.
O algoritmo que usamos para É um dos algoritmos de ordenação mais fáceis de entender. Uma forma de projetar um algoritmo é confiar na definição do problema. Nesse caso, o problema é ordenar um vetor do menor para o maior. Isso significa rearranjar os valores de modo que a[0] seja o menor, a[1] o próximo, e assim por diante. Essa definição fornece um esboço para o algoritmo de ordenação por seleção : for
(int indice = 0; indice < numeroUsado; indice++) Colocar o indice menor elemento em a[indice]
Há muitas formas de se compreender esta abordagem geral. Os detalhes poderiam ser desenvolvidos com a utilização de dois vetores e copiando-se os elementos de um para o outro em ordem, mas utilizar apenas um vetor é adequado e econômico. Portanto, a função ordena utiliza apenas o vetor que contém os valores a serem ordenados. A função ordena rearranja os valores no vetor trocando pares de valores. Vamos analisar um exemplo concreto para que você veja como o algoritmo funciona. Considere o vetor mostrado no Painel 5.7. O algoritmo colocará o menor valor em a[0]. O menor valor é o valor em a[3], logo o algoritmo troca os valores de a[0] e a[3]. Então, o algoritmo procura pelo próximo elemento. O valor em a[0] é agora o menor elemento, e o próximo é o menor entre os elementos restantes, a[1], a[2], a[3] ,..., a[9]. No exemplo do Painel 5.7, o próximo elemento menor está em a[5], e o algoritmo troca os valores de a[1] e a[5]. Esse posicionamento do segundo menor elemento é ilustrado na
Programando com Vetores
137
quarta e quinta figuras de vetores no Painel 5.7. Então, o algoritmo posiciona o terceiro menor elemento, e assim por diante. À medida que a ordenação prossegue, os primeiros elementos do vetor são fixados na ordem correta de valores. A porção ordenada do vetor aumenta com o acréscimo, uns após os outros, dos elementos da porção não-ordenada do vetor. Observe que o algoritmo não precisa fazer nada com o valor da última variável indexada, a[9]. Uma vez que os outros elementos tenham sido posicionados corretamente, a[9] também deve estar com o valor correto. Afinal, o valor correto para a[9] é o menor valor restante a ser movido, e o único valor restante a ser movido é o valor que já está em a[9]. A definição da função ordena, incluída em um programa de demonstração, é dada no Painel 5.8. ordena utiliza a função indiceDoMenor para encontrar o índice do menor elemento na extremidade não-ordenada do vetor e depois efetua a troca para mover o próximo elemento menor para o lado ordenado do vetor. A função trocaValores, mostrada no Painel 5.8, é usada para trocar os valores das variáveis indexadas. Por exemplo, a chamada seguinte trocará os valores de a[0] e a[3]: trocaValores(a[0], a[3]); A função trocaValores foi explicada
Painel 5.7
Ordenação por seleção
Painel 5.8
Ordenação por seleção ( parte 1 de 3)
no Capítulo 4.
1 2 3
//Testa o procedimento sort. #include using namespace std;
4 5 6 7 8 9 10 11 12 13
void fillArray(int a[], int size, int& numberUsed); //Pré-condição: size é o tamanho declarado do vetor a. //Pós-condição: numberUsed é o número de valores armazenado em a. //a[0] até a[numberUsed - 1] foi preenchido com //inteiros não-negativos lidos a partir do teclado. void sort(int a[], int numberUsed); //Pré-condição: numberUsed é <= ao tamanho declarado de a. //Os elementos de vetor a[0] até a[numberUsed - 1] possuem valores. //Pós-condição: Os valores de a[0] até a[numberUsed - 1] foram //rearranjados, de modo que a[0] <= a[1] <= ... <= a[numberUsed - 1].
14 void swapValues(int& v1, int& v2); 15 //Troca os valores de v1 e v2. 16 int indexOfSmallest(const int a[], int startIndex, int numberUsed); 17 //Pré-condição: 0 <= startIndex < numberUsed. Os elementos do vetor de referência 18 //possuem valores. Fornece o índice i, tal que a[i] é o menor dentre os
138
Vetores
Painel 5.8
Ordenação por seleção ( parte 2 de 3)
19
//valores a[startIndex], a[startIndex + 1], ..., a[numberUsed - 1].
20 21 22
int main(
cout << "Este programa ordena números do menor para o maior.\n";
23 24 25
int sampleArray[10],
numberUsed; fillArray(sampleArray, 10, numberUsed); sort(sampleArray, numberUsed);
26 27 28 29 30 31 32 33
)
{
cout << "Em ordem ascendente, os números são:\n"; = 0; index < numberUsed; index++) cout << sampleArray[index] << " "; cout << endl;
for (int index
return 0;
} void fillArray(int a[], int size, int&
numberUsed)
34 void sort(int a[], int numberUsed) 35 { 36 int indexOfNextSmallest; 37 for (int index = 0; index < numberUsed - 1; index++) 38 {//Coloca o valor correto em a[index]: 39 indexOfNextSmallest = 40 indexOfSmallest(a, index, numberUsed); 41 swapValues(a[index], a[indexOfNextSmallest]); 42 //a[0] <= a[1] <=...<= a[index] são os menores elementos do vetor 43 //original. O resto dos elementos estão nas posições remanescentes. 44 } 45 } 46 void swapValues(int& v1, 47 { 48 int temp; 49 temp = v1; 50 v1 = v2; 51 v2 = temp; 52 } 53
int &
v2)
54 int indexOfSmallest(const int a[], int startIndex, int numberUsed) 55 { 56 int min = a[startIndex], 57 indexOfMin = startIndex; 58 for (int index = startIndex + 1; index < numberUsed; index++) 59 if (a[index] < min) 60 { 61 min = a[index]; 62 indexOfMin = index; 63 //min é o menor de a[startIndex] até a[index] 64 } 65 66 }
return indexOfMin;
Vetores Multidimensionais Painel 5.8
139
Ordenação por seleção ( parte 3 de 3)
DIÁLOGO PROGRAMA-USUÁRIO Este programa ordena números do menor para o maior. Forneça até 10 números inteiros não-negativos. Assinale o final da lista com um número negativo. 80 30 50 70 60 90 20 30 40 -1
Em ordem ascendente, os números são: 20 30 40 50 60 70 80 90
17. Escreva um programa que leia até dez números inteiros não-negativos de um vetor chamado numeroVetor e depois escreva esses números na tela. Para este exercício você não precisa usar nenhuma função. É apenas um programa-brinquedo e pode ser bem pequeno. 18. Escreva um programa que leia até dez letras de um vetor e escreva as letras na tela em ordem inversa. Por exemplo, se a entrada for abcd.
a saída deve ser dcba
Utilize um ponto final como sentinela para marcar o fim da entrada. Chame o vetor de caixaDeLetras. Não precisa utilizar nenhuma função. É apenas um programa-brinquedo e pode ser bem pequeno. 19. Abaixo está a declaração para uma versão alternativa da função busca definida no Painel 5.6. A fim de utilizar esta versão alternativa da função busca, precisaríamos reescrever alguns trechos do programa, mas para este exercício tudo o que você precisa fazer é escrever a definição de função para esta versão alternativa de busca. numeroUsado, onde); //Pré-condição: numeroUsado é <= ao tamanho declarado do //vetor a. Além disso, a[0] até a[numeroUsado - 1] possuem valores. //Pós-condição: Se alvo é um dos elementos de a[0] //até a[numeroUsado - 1], então essa função é avaliada como //true e fixa o valor de onde de modo que a[onde] == //alvo; caso contrário, essa função é avaliada como false e o //valor de onde fica inalterado. bool
busca(const
int
a[],
int
int alvo, int&
5.4
Vetores Multidimensionais
O C++ permite que se declarem vetores com mais de um índice. Esta seção descreve esses vetores multidimensionais. ■ FUNDAMENTOS DOS VETORES MULTIDIMENSIONAIS
Às vezes é útil ter um vetor com mais de um índice, e isso é permitido em C++. A linha seguinte declara um vetor de caracteres chamado pagina. O vetor pagina possui dois índices. O primeiro vai de 0 a 29, e o segundo de 0 a 99. char
pagina[30][100];
Cada uma das variáveis indexadas para este vetor possui dois índices. Por exemplo, pagina[0][0], pagina[15][32] e pagina [29][99] são três das variáveis indexadas para este vetor. Observe que cada índice deve estar dentro de seus próprios colchetes. Como acontecia com os vetores de uma dimensão, que já estudamos, cada variável indexada para um vetor multidimensional é uma variável do tipo-base. Um vetor pode ter qualquer número de índices, mas talvez o número mais comum seja dois. Um vetor bidimensional pode ser visualizado como uma apresentação de duas dimensões em que o primeiro índice fornece a li-
140
Vetores
nha, e o segundo, a coluna. Por exemplo, as variáveis indexadas do vetor bidimensional pagina podem ser visualizadas da seguinte forma: pagina[0][0], pagina[0][1], ..., pagina[0][99] pagina[1][0], pagina[1][1], ..., pagina[1][99] pagina[2][0], pagina[2][1], ..., pagina[2][99] . . . pagina[29][0], pagina[29][1], ..., pagina[29][99]
Você pode utilizar o vetor pagina pra armazenar todos os caracteres de uma página de texto que possua trinta linhas (numeradas de 0 a 29) e 100 caracteres em cada linha (numerados de 0 a 99). Em C++, um vetor bidimensional, como pagina, é na verdade um vetor de vetores. O vetor pagina acima é, na realidade, um vetor unidimensional de tamanho 30, cujo tipo-base é um vetor de caracteres unidimensional de tamanho 100. Normalmente, isso não deve ser motivo de preocupação, e você pode agir como se o vetor pagina fosse mesmo um vetor com dois índices (em vez de um vetor de vetores, o que é mais difícil de entender). Há, todavia, pelo menos uma situação em que um vetor bidimensional se parece muito com um vetor de vetores: quando se tem uma função com um parâmetro vetorial para um vetor bidimensional, o que será discutido na próxima subseção. DECLARAÇÃO DE VETOR MULTIDIMENSIONAL SINTAXE Tipo Nome_Vetor[Tamanho_Dimensao_1] [Tamanho_Dimensao_2] ... [Tamanho_Dimensao_Final]
EXEMPLOS pagina[30][100]; int matriz[2][3]; double tresDImagem[10][20][30]; Uma declaração de vetor da forma mostrada acima definirá uma variável indexada para cada combinação de índices vetoriais. Por exemplo, a segunda das declarações acima define as seis variáveis indexadas seguintes para o vetor matriz: matriz[0][0], matriz[0][1], matriz[0][2], matriz[1][0], matriz[1][1], matriz[1][2] char
PARÂMETROS DE VETORES MULTIDIMENSIONAIS A seguinte declaração de um vetor bidimensional declara, na realidade, um vetor unidimensional de tamanho 30 cujo tipo-base é um vetor unidimensional de caracteres de tamanho 100. ■
char
pagina[30][100];
Visualizar um vetor bidimensional como um vetor de vetores o ajudará a entender como o C++ lida com os parâmetros de vetores multidimensionais. Por exemplo, a função seguinte toma um vetor, como pagina, e o imprime na tela: void
exibePagina(const
char
p[][100],
int
tamanhoDimensao1)
{ (int indice1 = 0; indice1 < tamanhoDimensao1; indice1++) {//Imprimindo uma linha: for (int indice2 = 0; indice2 < 100; indice2++) cout << p[indice1][indice2]; cout << endl; } for
}
Observe que, com um parâmetro vetorial bidimensional, o tamanho da primeira dimensão não é dado, e precisamos incluir um parâmetro int para fornecer o tamanho da primeira dimensão. (Como com vetores comuns, o compilador permitirá que você especifique a primeira dimensão, colocando um número dentro do primeiro par de colchetes. Entretanto, tal número é apenas um comentário; o compilador o ignora.) O tamanho da segunda di-
Vetores Multidimensionais
141
mensão (e todas as outras dimensões, se houver mais do que duas) é dado depois do parâmetro vetorial, como mostrado pelo parâmetro const char p[][100]
Se você compreende que um vetor multidimensional é um vetor de vetores, essa regra começa a fazer sentido. Como o parâmetro vetorial bidimensional const char p[][100]
é um parâmetro para um vetor de vetores, a primeira dimensão é na realidade o índice do vetor e é tratado exatamente como um índice de vetor para um vetor comum, unidimensional. A segunda dimensão é parte da descrição do tipo-base, que é um vetor de caracteres de tamanho 100. PARÂMETROS VETORIAIS MULTIDIMENSIONAIS Quando um parâmetro vetorial multidimensional é dado em um cabeçalho ou declaração de função, o tamanho da primeira dimensão não é dado, mas os tamanhos remanescentes precisam ser dados entre colchetes. Como o tamanho da primeira dimensão não é dado, normalmente você precisa de um parâmetro adicional de tipo int que fornece o tamanho desta primeira dimensão. A seguir há um exemplo de uma declaração de função com um parâmetro vetorial bidimensional p: void recebePagina( char p[][100], int tamanhoDimensao1);
PROGRAMA BIDIMENSIONAL DE NOTAS ESCOLARES O Painel 5.9 contém um programa que utiliza um vetor bidimensional chamado notas para armazenar e depois exibir as notas de uma classe pequena. A classe tem quatro alunos, e os registros incluem três provas. O Painel 5.10 ilustra como o vetor notas é usado para armazenar dados. O primeiro índice é usado para designar um aluno, e o segundo é usado para designar uma prova. Como alunos e provas são numerados a partir do 1 e não do 0, devemos subtrair 1 do número dos alunos e do número da prova para obter a variável indexada que armazena uma nota de prova em particular. Por exemplo, a nota que o aluno de número 4 recebeu na prova de número 1 é registrada em nota[3][0] . Nosso programa também usa dois vetores comuns unidimensionais. O vetor aluMed será usado para registrar a nota média para cada um dos alunos. Por exemplo, o programa fixará aluMed[0] como igual à média das notas de prova recebidas pelo aluno 1, aluMed[1] como igual à média das notas de prova recebidas pelo aluno 2, e assim por diante. O Painel 5.10 ilustra a relação entre os vetores notas , aluMed e provaMed . Esse Painel mostra alguns dados de amostra para o vetor notas. Esses dados, por sua vez, determinam os valores que o programa armazena em aluMed e em provaMed . O Painel 5.11 também mostra esses valores, que o programa calcula para aluMed e provaMed . O programa completo para preencher o vetor notas e depois calcular e exibir tanto as médias dos alunos quanto as médias das provas é mostrado no Painel 5.9. Nesse programa, declaramos as dimensões do vetor como constantes nomeadas globais. Como os procedimentos são específicos para este programa e não podem ser reutilizados em outro lugar, utilizamos essas constantes definidas globalmente nos corpos dos procedimentos, em vez de ter parâmetros para o tamanho das dimensões do vetor. Como isso é rotina, o painel não mostra o código que preenche o vetor.
Painel 5.9
1 2 3 4 5 6 7 8 9 10 11 12 13
Vetor bidimensional ( parte 1 de 3) //Lê pontuações em provas para cada aluno em um vetor bidimensional de notas (mas o código //de entrada não é mostrado no painel). Calcula a pontuação média para cada aluno e a //pontuação média para cada prova. Exibe as pontuações em cada prova e as médias. #include #include using namespace std; const int NUMBER_STUDENTS = 4, NUMBER_QUIZZES = 3; void computeStAve(const int grade[][NUMBER_QUIZZES], double stAve[]);
//Pré-condição: As constantes globais NUMBER_STUDENTS e NUMBER_QUIZZES //são as dimensões do vetor notas. Cada uma das variáveis indexadas //grade[stNum-1, quizNum-1] contém a pontuação para o aluno stNum na prova quizNum. //Pós-condição: Cada stAve[stNum-1] contém a média para o aluno número stNum.
142
Vetores
Painel 5.9
Vetor bidimensional ( parte 2 de 3)
14 15 16 17 18 19
void
computeQuizAve(const int grade[][NUMBER_QUIZZES] , double quizAve[]); //Pré-condição: As constantes globais NUMBER_STUDENTS e NUMBER_QUIZZES //são as dimensões do vetor notas. Cada uma das variáveis indexadas //grade[stNum-1, quizNum-1] contém a pontuação para o aluno stNum na prova quizNum. //Pós-condição: Cada quizAve[quizNum-1] contém a média para a prova número //quizNum.
20 21 22 23 24 25 26 27
void display( const int grade[][NUMBER_QUIZZES], const double stAve[], const double quizAve[]);
//Pré-condição: As constantes globais NUMBER_STUDENTS e NUMBER_QUIZZES //são as dimensões do vetor notas. Cada uma das variáveis indexadas grade[stNum-1, //quizNum-1] contém a pontuação para o aluno stNum na prova quizNum. Cada //stAve[stNum-1] contém a média para o aluno stNum. Cada quizAve[quizNum-1] //contém a média para a prova número quizNum. //Pós-condição: Todos os dados em grade, stAve e quizAve são mostrados na tela.
28 int main( ) 29 { 30 int grade[NUMBER_STUDENTS][NUMBER_QUIZZES]; double 31 stAve[NUMBER_STUDENTS]; double quizAve[NUMBER_QUIZZES]; 32 33 34 35 36 computeStAve(grade, stAve); 37 computeQuizAve(grade, quizAve); 38 display(grade, stAve, quizAve); return 0; 39 40 } 41 void computeStAve(const int grade[][NUMBER_QUIZZES], double stAve[]) 42 { for (int stNum = 1; stNum <= NUMBER_STUDENTS; stNum++) 43 44 {//Processa um stNum: double sum = 0; 45 for (int quizNum = 1; quizNum <= NUMBER_QUIZZES; quizNum++) 46 47 sum = sum + grade[stNum-1][quizNum-1]; 48 //sum contém a soma das pontuações nas provas para o aluno número stNum. 49 stAve[stNum-1] = sum/NUMBER_QUIZZES; 50 //A média para o aluno stNum é o valor de stAve[stNum-1] 51 } 52 } 53 void computeQuizAve(const int grade[][NUMBER_QUIZZES], double quizAve[]) 54 { for (int quizNum = 1; quizNum <= NUMBER_QUIZZES; quizNum++) 55 56 {//Processa uma prova (para todos os alunos): double sum = 0; 57 for (int stNum = 1; stNum <= NUMBER_STUDENTS; stNum++) 58 59 sum = sum + grade[stNum-1][quizNum-1]; 60 //sum contém a soma das pontuações de todos os alunos nas provas número quizNum. 61 quizAve[quizNum-1] = sum/NUMBER_STUDENTS; 62 //A média para a prova quizNum é o valor de quizAve[quizNum-1] 63 } 64 } 65 66
void display(const int grade[][NUMBER_QUIZZES],
const double stAve[], const double quizAve[])
Vetores Multidimensionais
Painel 5.9
Vetor bidimensional ( parte 3 de 3)
67 { 68 69 70
cout.setf(ios::fixed); cout.setf(ios::showpoint); cout.precision(1);
71 72 73 74 75 76 77 78 79 80 81
cout << setw(10) << "Aluno" << setw(5) << "Média" << setw(15) << "Provas\n"; for (int stNum = 1; stNum <= NUMBER_STUDENTS; stNum++) {//Exibição na tela para stNum: cout << setw(10) << stNum << setw(5) << stAve[stNum-1] << " "; for (int quizNum = 1; quizNum <= NUMBER_QUIZZES; quizNum++) cout << setw(5) << grade[stNum-1][quizNum-1]; cout << endl; }
82 83 84 85 86 }
cout << "Médias das provas = "; (int quizNum = 1; quizNum <= NUMBER_QUIZZES; quizNum++) cout << setw(5) << quizAve[quizNum-1]; cout << endl;
for
DIÁLOGO PROGRAMA-USUÁRIO
Aluno Média 1 10.0 2 1.0 3 7.7 4 7.3 Médias das provas =
Painel 5.10
Vetor bidimensional
10 2 8 8 7.0
Provas 10 10 0 1 6 9 4 10 5.0 7.5
notas
prova 1
prova 2
prova 3
aluno 1 aluno 2 aluno 3 aluno 4
nota [3][0] é a nota que o aluno 4 recebeu na prova 1.
nota [3][1] é a nota que o aluno 4 recebeu na prova 2.
nota [3][2] é a nota que o aluno 4 recebeu na prova 3.
143
144
Vetores
Painel 5.11
Vetor bidimensional notas
prova 1 prova 2
prova 3
aluno 1 aluno 2 aluno 3 aluno 4
20. Qual é a saída produzida pelo seguinte código? int
meuVetor[4][4], indice1, indice2;
for
(indice1 = 0; indice1 < 4; indice1++) for
(indice2 = 0; indice2 < 4; indice2++) meuVetor[indice1][indice2] = indice2;
for
(indice1 = 0; indice1 < 4; indice1++)
{ for (indice2
= 0; indice2 < 4; indice2++)
cout << meuVetor[indice1][indice2] << " "; cout << endl; }
21. Escreva código para preencher o vetor a (declarado abaixo) com números digitados no teclado. Serão fornecidos cinco números por linha, em quatro linhas (embora nossa solução não dependa obrigatoriamente de como os números da entrada são divididos em linhas). int
a[4][5];
22. Escreva uma definição de função para uma função void chamada eco de tal forma que a seguinte chamada de função ecoe a entrada descrita no Exercício de Autoteste 21 e no mesmo formato que especificamos para a entrada (ou seja, quatro linhas de cinco números por linha): eco(a, 4);
■ ■
■
■
■
Um vetor pode ser usado para armazenar e manipular uma coleção de dados que sejam todos de mesmo tipo. As variáveis indexadas de um vetor podem ser usadas exatamente como quaisquer outras variáveis do tipobase do vetor. Um loop for é uma boa forma de se percorrer os elementos de um vetor e executar alguma ação do programa sobre cada variável indexada. O erro mais comum em programação é cometido quando se utilizam vetores tentando ter acesso a índices vetoriais inexistentes. Verifique sempre a primeira e a última iterações de um loop que manipule um vetor para garantir que não seja usado um índice ilegalmente pequeno ou grande. Um parâmetro formal vetorial não é um parâmetro chamado por valor nem um parâmetro chamado por referência, e sim um novo tipo de parâmetro. Um parâmetro vetorial é semelhante a um parâmetro chama-
Respostas dos Exercícios de Autoteste
■
■
■
■
145
do por referência no sentido de que qualquer mudança no parâmetro formal no corpo da função será feita no argumento vetorial quando a função for chamada. As variáveis indexadas de um vetor são armazenadas umas ao lado das outras na memória do computador, de modo que o vetor ocupa uma porção contígua da memória. Quando o vetor é transmitido como argumento para uma função, apenas o endereço da primeira variável indexada (numerada como 0) é fornecido à função que faz a chamada. Portanto, a função com um parâmetro vetorial normalmente precisa de outro parâmetro formal de tipo int para fornecer o tamanho do vetor. Quando se usa um vetor parcialmente preenchido, seu programa necessita de uma variável adicional de tipo int para controlar quanto do vetor é usado. Para dizer ao compilador que um argumento vetorial não deve ser alterado pela sua função, você pode inserir o modificador const antes do parâmetro vetorial para aquela posição de argumento. Um parâmetro vetorial que é modificado com um const é chamado de parâmetro vetorial constante. Se você precisar de um vetor com mais de um índice, utilize um vetor multidimensional, que, na verdade, é um vetor de vetores.
RESPOSTAS DOS EXERCÍCIOS DE AUTOTESTE 1. int a[5] é uma declaração em que 5 é o número de elementos do vetor. A expressão a[4] é um acesso ao vetor definido pela declaração anterior. O acesso é para o elemento com o índice 4, que é o quinto (e último) elemento do vetor. 2. a. nota b. double c. 5 d. de 0 a 4 e. nota[0], nota[1], nota[2], nota[3] ou nota[4] 3. a. Um inicializador a mais. b. Correto. O tamanho do vetor é 4. c. Correto. O tamanho do vetor é 4. 4. abc 5. 1.1 2.2 3.3 1.1 3.3 3.3
(Lembre-se de que os índices começam com 0, não com 1.) 6. 0 2 4 6 8 10 12 14 16 18 0 4 8 12 16
7. As variáveis indexadas de amostraVetor vão de amostraVetor[0] até amostraVetor[9], mas esse trecho de código tenta preencher de amostraVetor[1] até amostraVetor[10]. O índice 10 em amostraVetor[10] está fora do intervalo. 8. Há um índice fora do intervalo. Quando indice é igual a 9, indice + 1 é igual a 10, a[indice + 1], que é o mesmo que a[10], possui um índice ilegal. O loop deveria acabar uma iteração antes. Para corrigir o código, mude a primeira linha do loop for para (int indice = 0; indice < 9; indice++) 9. int i, a[20]; cout << "Forneça 20 números:\n"; for (i = 0; i < 20; i++) cin >> a[i]; for
10. O vetor consumirá 14 bytes de memória. O endereço da variável indexada seuVetor[3] é 1006. 11. As seguintes chamadas de função são aceitáveis: triplicador(a[2]); triplicador(a[numero]); triplicador(numero);
As seguintes chamadas de função são incorretas: triplicador(a[3]); triplicador(a);
146
Vetores
A primeira possui um índice ilegal. A segunda não possui nenhuma expressão indexada. Não se pode usar um vetor completo como um argumento de triplicador, como na segunda chamada. A seção Vetores Completos como Argumentos de Função discute uma situação diferente em que você pode usar um vetor completo como argumento. 12. O loop passa por variáveis indexadas, de b[1] a b[5], mas 5 é um índice ilegal para o vetor b. Os índices são 0, 1, 2, 3 e 4. A versão correta do código é dada abaixo: int b[5] = {1, 2, 3, 4, 5}; for (int i = 0; i < 5; i++)
triplicador(b[i]); 13. void maisUm(int a[], int tamanho) //Pré-condição: tamanho é o tamanho declarado do vetor a. //a[0] até a[tamanho-1] receberam valores. //Pós-condição: a[indice] é aumentado em 1 //para todas as variáveis indexadas de a. { for (int indice = 0; indice < size; indice++) a[indice] = a[indice] + 1; }
14. As seguintes chamadas de função são aceitáveis: tambem2(meuVetor, 29); tambem2(meuVetor, 10); tambem2(seuVetor, 100);
A chamada tambem2(meuVetor, 10);
é legal, mas preencherá apenas as primeiras dez variáveis indexadas de meuVetor. Se for isso o desejado, a chamada é aceitável. As seguintes chamadas de função são incorretas: tambem2(meuVetor, 55); "Ei tambem2. Por favor venha aqui." tambem2(meuVetor[3], 29);
A primeira destas é incorreta porque o segundo argumento é muito extenso, a segunda porque está faltando o ponto-e-vírgula final (e por outras razões) e a terceira porque utiliza uma variável indexada para um argumento, quando deveria utilizar o vetor completo. 15. Você pode transformar o parâmetro vetorial saida em parâmetro constante, já que não há necessidade de alterar os valores de qualquer variável indexada do parâmetro vetorial. Não se pode transformar o parâmetro descartaImpar em parâmetro constante porque os valores de algumas variáveis indexadas podem ser alterados. void saida(const double a[], int tamanho);
//Pré-condição: a[0] até a[tamanho - 1] possuem valores. //Pós-condição: a[0] até a[tamanho - 1] foram escritos. void descartaImpar(int a[], int tamanho);
//Pré-condição: a[0] até a[tamanho - 1] possuem valores. //Pós-condição: Todos os números ímpares em a[0] até a[tamanho - 1] // foram alterados para 0. 16. int foraDeOrdem(double vetor[], int tamanho) { for (int i = 0; i < tamanho - 1; i++) if (vetor[i] > vetor [i+1] //extrai a[i+1] para cada i. return i+1; return -1; } 17. #include using namespace std; const int TAMANHO_DECLARADO = 10;
Respostas dos Exercícios de Autoteste int main( )
{ cout << "Forneça até dez inteiros não-negativos.\n" << "Coloque um número negativo ao final.\n"; int numeroVetor[TAMANHO_DECLARADO], proximo, indice = 0; cin >> proximo; while ( (proximo >= 0) && (indice < TAMANHO_DECLARADO) ) { numeroVetor[indice] = proximo; indice++; cin >> proximo; } int numeroUsado = indice;
cout << "Aqui estão eles de volta para você:"; for (indice = 0; indice < numeroUsado; indice++) cout << numeroVetor[indice] << " "; cout << endl; return 0; 18.
} #include using namespace std; const int TAMANHO_DECLARADO = 10; int main( )
{ cout << "Forneça até dez letras" << " seguidas por um ponto final:\n"; char caixaDeLetras [TAMANHO_DECLARADO], proxima; int indice = 0; cin >> proxima; while ( (proxima != ’.’) && (indice < TAMANHO_DECLARADO) ) { caixaDeLetras [indice] = proxima; indice++; cin >> proxima; } int numeroUsado = indice;
cout << "Aqui estão elas na ordem inversa:\n"; for (indice = numeroUsado-1; indice >= 0; indice--) cout << caixaDeLetras [indice]; cout << endl; return 0; } 19. bool
busca(const int a[], int numeroUsado, int alvo, int& onde)
{ int indice = 0; bool encontrado = false; while ((!encontrado) && (indice < numeroUsado)) if (alvo == a[indice]) encontrado = true; else
indice++; //Se alvo foi encontrado, então //encontrado == true e a[indice] == alvo.
147
148
Vetores
(encontrado) onde = indice; return encontrado; if
} 20. 0 0 0 0
1 1 1 1
2 3 2 3 2 3 2 3 21. int a[4][5]; int indice1, indice2; for (indice1 = 0; indice1 < 4; indice1++) for (indice2 = 0; indice2 < 5; indice2++) cin >> a[indice1][indice2]; 22. void eco(const int a[][5], int tamanhoDea) //Apresenta como saída os valores no vetor a em tamanhoDea linhas //com 5 números por linha. { for (int indice1 = 0; indice1 < tamanhoDea; indice1++) { for (int indice2 = 0; indice2 < 5; indice2++) cout << a[indice1][indice2] << " "; cout << endl; } }
PROJETOS DE PROGRAMAÇÃO 1. Escreva um programa que leia a quantidade média de chuva mensal de uma cidade para cada mês do ano e depois leia a quantidade real de chuva para cada um dos 12 meses anteriores. Assim, o programa imprime uma tabela bem formatada que mostra a quantidade de chuva para cada um dos 12 meses anteriores e também quão acima ou quão abaixo da média a quantidade de chuva foi a cada mês. A média mensal é dada pelos meses de janeiro, fevereiro e assim por diante, em seqüência. Para obter a quantidade de chuva real nos 12 meses anteriores, o programa primeiro pergunta qual é o mês atual e depois pede as cifras da quantidade de chuva nos 12 meses anteriores. A saída deve nomear corretamente os meses. Existem várias formas de se lidar com nomes de meses. Um método simples é codificar os meses como inteiros e depois fazer uma conversão antes de executar a saída. Um grande comando switch é aceitável em uma função de saída. A entrada dos meses pode ser tratada da maneira que você desejar, desde que seja relativamente fácil e agradável para o usuário. Depois que houver acabado o programa acima, produza uma versão aperfeiçoada que também apresente um gráfico exibindo a quantidade média e a quantidade real de chuva para cada um dos 12 meses anteriores. O gráfico deve ser similar àquele mostrado no Painel 5.4, a não ser pelo fato de que deve haver duas barras para cada mês e estas devem receber os rótulos de quantidade média de chuva e quantidade de chuva no último mês. Seu programa deve perguntar se o usuário deseja ver a tabela ou o gráfico de barras, e depois deve exibir o formato requisitado. Inclua um loop que permita que o usuário veja um ou outro formato quantas vezes desejar até requisitar que o programa se encerre. 2. Escreva uma função chamada apagaRepetidas que tenha um vetor de caracteres parcialmente preenchido como parâmetro formal e que remova todas as letras repetidas do vetor. Como um vetor parcialmente preenchido exige dois argumentos, a função na realidade terá dois parâmetros formais: um parâmetro vetorial e um parâmetro formal de tipo int que fornece o número de posições vetoriais usadas. Quando uma letra é removida, as letras restantes são movidas para a frente para preencher as vagas. Isso criará posições vazias no final do vetor, de modo que uma parte menor do vetor é utilizada. Como o parâmetro formal é um vetor parcialmente preenchido, um segundo parâmetro formal de tipo int dirá quantas posições do vetor são preenchidas. Este segundo parâmetro formal será um parâmetro chamado por referência
Projetos de Programação
149
e será alterado para mostrar quanto do vetor é usado depois que as letras repetidas são removidas. Por exemplo, considere o seguinte código: a[10]; a[0] = ’a’; a[1] = ’b’; a[2] = ’a’; a[3] = ’c’; int tamanho = 4; apagaRepetidas(a, tamanho); char
Depois que esse código é executado, o valor de a[0] é ’a’, o valor de a[1] é ’b’, o valor de a[2] é ’c’ e o valor de tamanho é 3. (O valor de a[3] não importa, já que o vetor parcialmente preenchido não utiliza mais essa variável indexada.) Você pode assumir que o vetor parcialmente preenchido contenha apenas letras minúsculas. Insira sua função em um programa-teste adequado. 3. O desvio-padrão de uma lista de números é a medida de quanto os números se desviam da média. Se o desvio-padrão é pequeno, os números estão aglomerados junto à média. Se o desvio-padrão é grande, os números estão dispersos em relação à média. O desvio-padrão, S , de uma lista de N números x i é definido da seguinte forma:
em que x – é a média de N números x 1, x 2, ... Defina uma função que tome um vetor parcialmente preenchido de números e seu argumento e retorne o desvio-padrão dos números no vetor parcialmente preenchido. Como um vetor parcialmente preenchido requer dois argumentos, a função, na realidade, terá dois parâmetros formais: um parâmetro vetorial e um parâmetro formal de tipo int que fornece o número de posições vetoriais utilizado. Os números no vetor serão de tipo double. Insira sua função em um programa-teste adequado. 4. Escreva um programa que leia um vetor de tipo int. Você pode presumir que existam menos de 50 elementos no vetor. O programa determina quantas posições são ocupadas. A saída deve ser em uma lista de duas colunas. A primeira coluna é uma lista dos diferentes elementos do vetor; a segunda coluna é a contagem do número de ocorrências de cada elemento. A lista deve ser ordenada com base na primeira coluna, do maior para o menor. Para os seguintes valores: -12 3 -12 4 1 1 -12 1 -1 1 2 3 4 2 3 -12
a saída deve ser N 4 3 2 1 -1 -12
Count 2 3 2 4 1 4
5. Um vetor pode ser usado para armazenar grandes inteiros, um dígito de cada vez. Por exemplo, o inteiro 1234 pode ser armazenado no vetor a fixando-se a[0] como 1, a[1] como 2, a[2] como 3 e a[3] como 4. Entretanto, para este exercício você pode achar mais útil armazenar os dígitos de trás para a frente, ou seja, colocar o 4 em a[0], o 3 em a[1], o 2 em a[2] e o 1 em a[3]. Neste exercício você escreverá um programa que leia dois inteiros positivos de 20 ou menos dígitos de comprimento e depois apresente a soma dos dois números. O programa lerá os dígitos como valores do tipo char, de forma que o número 1234 é lido como os quatro caracteres ’ 1’, ’2’, ’3’ e ’4’. Depois de serem lidos pelo programa, os caracteres serão alterados para valores de tipo int. Os dígitos serão lidos e inseridos em um vetor parcialmente preenchido, e talvez você ache útil inverter a ordem dos elementos no vetor depois que este seja preenchi-
150
Vetores
do com os dados do teclado. (A opção entre inverter ou não a ordem dos elementos no vetor é sua. O programa pode ser feito dos dois modos, e cada um tem suas vantagens e desvantagens.) O programa executará a adição, implementando o algoritmo comum da adição. O resultado da adição é armazenado em um vetor de tamanho 20 e, então, o resultado é escrito na tela. Se o resultado da adição é um inteiro com mais do que o número máximo de dígitos (ou seja, mais de 20 dígitos), seu programa deve emitir uma mensagem dizendo que encontrou um "estouro de inteiros". Você deve ser capaz de alterar o comprimento máximo dos inteiros mudando apenas uma constante definida globalmente. Inclua um loop que permita ao usuário continuar a fazer adições até dizer que o programa deve ser encerrado. 6. Escreva um programa que permita dois usuários jogar o jogo-da-velha. O programa deve pedir que os jogadores X e O informem os lances alternadamente. O programa exibe as posições do jogo da seguinte forma: 1
2
3
4
5
6
7
8
9
O jogador faz o lance informando o número da posição que deseja assinalar. Após cada lance, o programa exibe o tabuleiro. Um exemplo de configuração de tabuleiro: X
X
O
4
5
6
O
8
9
7. Escreva um programa para atribuir assentos a passageiros em um avião. Considere um avião pequeno com assentos numerados da seguinte forma; 1
A B
C D
2
A B
C D
3
A B
C D
4
A B
C D
5
A B
C D
6
A B
C D
7
A B
C D
O programa deve exibir o padrão dos assentos, com um ’X’ assinalando os assentos já atribuídos. Por exemplo, depois que os assentos 1A, 2B e 4C já foram atribuídos, o padrão deve ser o seguinte: 1
X B
C D
2
A X
C D
3
A B
C D
4
A B
X D
5
A B
C D
6
A B
C D
7
A B
C D
Depois de mostrar os assentos disponíveis, o programa pede que o usuário indique o assento desejado, o usuário digita a informação pedida e depois o quadro de assentos disponíveis é atualizado. Isso prossegue até que todos os assentos sejam ocupados ou até o usuário pedir que o programa termine. Se o usuário escolher um assento já atribuído, o programa deve dizer que o assento está ocupado e pedir que o usuário escolha outro. 8. Escreva um programa que aceite dados de entrada como o programa no Painel 5.4 e que apresente como saída um gráfico de barras como o daquele programa, a não ser pelo fato de que seu programa apresentará as barras verticalmente e não horizontalmente. Um vetor bidimensional pode ser útil. 9. O matemático John Horton Conway inventou o "Jogo da Vida". Embora não seja um "jogo" no sentido tradicional, ele apresenta um comportamento interessante, especificado com poucas regras. Esse projeto pede que você escreva um programa que lhe permita especificar uma configuração inicial. O programa segue as regras da Vida (listadas brevemente) para mostrar o comportamento contínuo da configuração. VIDA é um organismo que vive em um mundo distinto, bidimensional. Embora esse mundo seja, na realidade, ilimitado, não temos toda essa liberdade e, assim, restringimos o vetor a 80 caracteres de largura e 22 de altura. Se você tem acesso a uma tela maior, use-a!
Projetos de Programação
151
Esse mundo é um vetor em que cada célula é capaz de abrigar uma célula da VIDA. As gerações marcam a passagem do tempo. Cada geração traz nascimentos e mortes para a comunidade da VIDA. Os nascimentos e mortes seguem o conjunto de regras: 1. Cada célula possui oito células vizinhas. As vizinhas de uma célula são as células diretamente acima, abaixo, à direita, à esquerda, diagonalmente acima à direita ou à esquerda e diagonalmente abaixo à direita ou à esquerda. 2. Se uma célula ocupada não possui vizinhas ou possui apenas uma, morre de solidão. Se uma célula ocupada possui mais de três vizinhas, morre de superpopulação. 3. Se uma célula vazia possui exatamente três células vizinhas ocupadas, há o nascimento de uma nova célula para substituir a célula vazia. 4. Nascimentos e mortes são instantâneos e ocorrem com as mudanças de geração. Uma célula que morre por qualquer razão pode ajudar a provocar o nascimento, mas uma célula recém-nascida não pode ressuscitar uma célula que está morrendo, nem a morte de uma célula impede a morte de outra, digamos, por meio da redução da população local. Exemplos:
*** vira
* * *
depois vira
***
de novo e assim por diante.
Observações: algumas configurações crescem a partir de configurações iniciais bem pequenas. Outras se des-
locam pela região. Recomenda-se que, para a saída de texto, você utilize um vetor retangular de char com 80 colunas e 22 linhas para armazenar as sucessivas gerações mundiais de VIDA. Utilize um * para indicar uma célula viva e um espaço em branco para indicar uma célula vazia (ou morta). Se você possui uma tela com mais linhas do que isso, não hesite em utilizar a tela toda. Sugestões: procure configurações estáveis. Ou seja, procure por comunidades que repitam os padrões continuamente. O número de configurações na repetição é chamado de período . Há configurações que são fixas, ou seja, que permanecem sem mudança. Um plano possível é encontrar essas configurações. Dicas: defina uma função void chamada geração que tome o vetor que chamamos mundo, um vetor de tipo char de 80 colunas por 22 linhas, que contenha a configuração inicial. A função percorre o vetor e modifica as células, assinalando as células com nascimentos e mortes de acordo com as regras listadas anteriormente. Isso envolve examinar uma célula de cada vez e ou matar a célula, ou deixá-la viver ou, se a célula estiver vazia, decidir se uma célula deve nascer. Deve haver uma função mostra que aceite o vetor mundo e exiba o vetor na tela. É preciso haver alguma espécie de intervalo de tempo entre as chamadas a geração e a mostra. Para fazer isso, seu programa deve gerar e mostrar a próxima geração quando se aperta a tecla Return. Você é livre para automatizar isso, mas a automação não é necessária para o programa.
Estruturas e Classes Estruturas e Classes 6Estruturas e Classes — Chegou a hora — disse a Morsa — De falar de muitas coisas: De sapatos, navios, lacres, De repolhos e de reis. Lewis Carroll, Através do Espelho
INTRODUÇÃO As classes talvez sejam o recurso mais importante que separa a linguagem C++ da linguagem C. Uma classe é um tipo cujos valores se chamam objetos . Os objetos possuem tanto funções de dados quanto funções-membros. As funções-membros têm acesso especial aos dados de seu objeto. Esses objetos são os objetos de programação orientada a objetos, uma filosofia de programação bastante popular e poderosa. Apresentaremos as classes em duas partes. Primeiro mostraremos como fornecer uma definição para uma estrutura. Uma estrutura (do tipo de que trataremos aqui) pode ser pensada como um objeto sem nenhuma função-membro. 1 A propriedade importante das estruturas é que os dados em uma estrutura podem ser uma coleção de dados de diversos tipos. Depois que você aprender como são as estruturas, a definição de classes virá como uma extensão natural. Você não precisa ter lido o Capítulo 5, sobre vetores, para ler o Capítulo 6 e a maior parte dos Capítulos 7 e 8, que tratam de classes. 6.1
Estruturas Eu não aceitaria participar de nenhum clube que me aceitasse como membro. Groucho Marx, The Groucho Letters
Às vezes é útil ter uma coleção de valores de tipos diferentes e tratar a coleção como um único item. Por exemplo, considere um certificado de depósito bancário (CDB). Um CDB é uma conta bancária que não permite retiradas por um número especificado de meses. Um CDB naturalmente possui três espécies de dados associados a ele: o saldo bancário, a taxa de juros e o prazo, que é o número de meses até a data do vencimento. Os primeiros dois itens podem ser representados por valores de tipo double, e o número de meses pode ser representado como um valor de tipo int. O Painel 6.1 mostra a definição de uma estrutura chamada CDBContaV1 que pode ser usada para esse tipo de conta. (O V1 significa "versão 1". Apresentaremos uma versão aperfeiçoada mais adiante neste mesmo capítulo.) 1.
Uma estrutura, na realidade, pode ter funções-membros em C++, mas não é este o enfoque que utilizaremos. Este detalhe é explicado mais adiante neste capítulo. Esta nota é apenas para que os leitores que pensaram haver encontrado um erro saibam que estamos conscientes da definição oficial de uma estrutura. A maioria dos leitores pode ignorar esta nota.
154
Estruturas e Classes
Painel 6.1
Definição de estrutura
1 2 3
//Programa para demonstrar o tipo de estrutura CDAccountV1. #include using namespace std;
4 5 6 7 8 9 10
//Estrutura para um certificado de depósito bancário: struct CDAccountV1 { double balance; double interestRate; int term;//meses até a data de vencimento };
Uma versão aperfeiçoada dessa estrutura será dada posteriormente neste capítulo.
11 void getData (CDAccountV1& theAccount); 12 //Pós-condição: theAccount.balance, theAccount.interestRate e 13 //theAccount.term receberam valores que o usuário informou ao teclado. 14 int main( ) 15 { 16 CDAccountV1 account; 17 getData(account); 18 19 20 21
rateFraction = account.interestRate/100.0; interest = account.balance*(rateFraction*(account.term/12.0)); account.balance = account.balance + interest;
22 23 24 25 26 27 28
cout.setf(ios::fixed); cout.setf(ios::showpoint); cout.precision(2); cout << "Após o prazo " << account.term << " meses,\n" << "você terá um saldo de $" << account.balance << endl;
29 30 31 32 33 34 35 36 37 38 39 40
double rateFraction, interest;
return 0; } //Utiliza iostream: void getData( CDAccountV1& theAccount) { cout << "Informe o seu saldo bancário: $"; cin >> theAccount.balance; cout << "Informe a taxa de juros da sua conta: "; cin >> theAccount.interestRate; cout << "Informe o número de meses até a data de vencimento: "; cin >> theAccount.term; }
DIÁLOGO PROGRAMA-USUÁRIO Informe o seu saldo bancário: $100.00 Informe a taxa de juros da sua conta: 10.0 Informe o número de meses até a data de vencimento: 6 Após o prazo de 6 meses, você terá um saldo de $105.00
Estruturas
155
■ TIPOS DE ESTRUTURAS
A definição de estrutura no Painel 6.1 é a seguinte: struct CDBContaV1
{ double saldo; double taxaDeJuro; int prazo;//meses
do prazo até a data de vencimento
};
A palavra-chave struct anuncia que essa é uma definição de tipo estrutura. O identificador CDBContaV1 é o nome do tipo estrutura. O identificador de estrutura pode ser qualquer identificador que não seja uma palavrachave. Embora isso não seja exigido pela linguagem C++, os identificadores de estrutura normalmente são escritos com uma letra maiúscula no início. Os identificadores declarados entre chaves, { }, são chamados membros de estrutura . Como ilustrado neste exemplo, uma definição de tipo estrutura termina com uma chave, }, e um pontoe-vírgula. Uma definição de estrutura normalmente é colocada fora de qualquer definição de função (da mesma forma que declarações de constantes globalmente definidas são colocadas fora de todas as definições de função). O tipo estrutura é, então, uma definição global disponível para todo o código que se segue à definição de estrutura. Uma vez que uma definição de tipo estrutura tenha sido dada, o tipo estrutura pode ser usado exatamente como os tipos predefinidos int, char e assim por diante. Observe que, no Painel 6.1, o tipo estrutura CDBContaV1 é usado para declarar uma variável na função main e como nome do tipo de parâmetro para a função getDados. Uma variável-estrutura pode guardar valores exatamente como qualquer outra variável. Um valor de estrutura é uma coleção de valores menores chamada de valores-membros. Há um valor-membro para cada nome de membro declarado na definição de estrutura. Por exemplo, um valor do tipo CDBContaV1 é uma coleção de três valoresmembros, dois de tipo double e um de tipo int. Os valores-membros que juntos constituem o valor de estrutura são armazenados em variáveis-membros, de que trataremos a seguir. Cada tipo de estrutura especifica uma lista de membros de estrutura. No Painel 6.1, a estrutura CDBContaV1 possui três membros de estrutura: saldo, taxaDeJuro e prazo. Cada um desses membros de estrutura pode ser usado para escolher uma variável menor que é parte da variável-estrutura maior. Essas variáveis menores são chamadas variáveis-membros. Variáveis-membros são especificadas fornecendo o nome da variável-estrutura seguido por um ponto e, depois, o nome do membro. Por exemplo, se conta é uma variável-estrutura do tipo CDBContaV1 (como declarado no Painel 6.1), a variável-estrutura conta possui as três seguintes variáveis-membros: conta.saldo conta.taxaDeJuro conta.prazo
As primeiras duas variáveis-membros são de tipo double, e a última, de tipo int. Como ilustrado no Painel 6.1, essas variáveis-membros podem ser usadas exatamente como quaisquer outras variáveis daqueles tipos. Por exemplo, a linha seguinte do programa no Painel 6.1 acrescentará o valor contido na variável-membro conta.saldo e o valor contido na variável comum juros e colocará o resultado na variável-membro conta.saldo: conta.saldo = conta.saldo + juros;
Dois ou mais tipos estrutura podem usar os mesmos membros de estrutura. Por exemplo, é perfeitamente legal ter as duas definições de tipo seguintes no mesmo programa: struct EstoqueDeFertilizantes
{ double quantidade; double conteudoNitrogenio;
};
e struct RendimentoDaColheita
{
156
Estruturas e Classes int quantidade; double tamanho;
}; OPERADOR PONTO
O operador ponto é utilizado para especificar uma variável-membro de uma variável-estrutura. Sintaxe
operador ponto
Nome_Variavel_Estrutura.Nome_Variavel_Membro EXEMPLOS struct NotaAluno
{ int NumeroAluno; char nota;
}; int main
( )
{ NotaAluno suaNota; suaNota.NumeroAluno = 2001; suaNota.nota = ’A’;
Alguns escritores chamam o operador ponto de operador de acesso aos membros da estrutura , mas não utilizaremos esse termo.
A coincidência de nomes não causará problemas. Por exemplo, se você declarar as duas variáveis-estruturas seguintes: EstoqueDeFertilizantes superCrescimento; RendimentoDaColheita bananas;
então a quantidade de fertilizante superCrescimento é armazenada na variável-membro superCrescimento.quantidade, e a quantidade de bananas produzida é armazenada na variável-membro bananas.quantidade. O operador ponto e a variável-estrutura especificam a que quantidade estamos nos referindo em cada exemplo. Um valor de estrutura pode ser visto como uma coleção de valores-membros. Um valor de estrutura também pode ser visto como um único (complexo) valor (que, por acaso, é constituído por valores-membros). Como um valor de estrutura pode ser visto como um valor único, valores de estrutura e variáveis-estruturas podem ser usadas da mesma forma que valores e variáveis simples dos tipos predefinidos como int. Em particular, pode-se atribuir valores de estrutura utilizando um sinal de igual. Por exemplo, se bananas e laranjas são variáveis-estruturas do tipo RendimentoDaColheita, já definido, então a seguinte linha é perfeitamente legal: bananas = laranjas;
A declaração de atribuição acima é equivalente a bananas.quantidade = laranjas.quantidade; bananas.tamanho = laranjas.tamanho; TIPOS ESTRUTURA SIMPLES
Define-se um tipo estrutura da forma mostrada a seguir. Identificador_Estrutura é o nome do tipo estrutura. SINTAXE struct
Identificador_Estrutura
{
Tipo_1 Variavel_Membro_Nome_1; Tipo_2 Variavel_Membro_Nome_2; . . .
Estruturas
157
(continuação) Tipo_Final Variavel_Membro_Nome_Fi nal; };
Não se esqueça deste ponto-e-vírgula.
EXEMPLO struct Automovel
{ int ano; int portas; double cavalosDoMotor; char modelo;
};
Não utilizaremos este recurso, mas você pode combinar membros de estrutura de mesmo tipo em uma lista única separada por vírgulas. Por exemplo, a seguinte definição é equivalente à definição de estrutura acima: struct Automovel
{ int ano,
portas;
double cavalosDoMotor; char modelo;
};
Variáveis de um tipo estrutura podem ser declaradas da mesma forma que variáveis de outros tipo. Por exemplo: Automovel meuCarro, seuCarro;
As variáveis-membros são especificadas por meio do operador ponto. Por exemplo: meuCarro.ano, meuCarro.portas, meuCarro.cavalosDoMotor e meuCarro.modelo.
ESQUECENDO UM PONTO-E-VÍRGULA EM UMA DEFINIÇÃO DE ESTRUTURA Quando você acrescenta a chave final, }, a uma definição de estrutura, parece que a definição de estrutura terminou, mas isso não é verdade. Você precisa colocar também um ponto-e-vírgula depois dessa chave final. Há um motivo para isso, embora esteja ligado a um recurso que não teremos a oportunidade de utilizar. Uma definição de estrutura é mais do que uma definição. Pode também ser usada para declarar variáveis-estruturas. Você pode listar nomes de variáveis-estruturas entre a chave final e o ponto-e-vírgula. Por exemplo, a definição seguinte é de uma estrutura chamada DadosClimaticos e declara duas variáveis-estruturas, dadosPonto1 e dadosPonto2, ambas de tipo DadosClimaticos: struct DadosClimaticos
{ double temperatura; double velocidadeDoVento; } dadosPonto1, dadosPonto2;
■
ESTRUTURAS COMO ARGUMENTOS DE FUNÇÃO
Uma função pode ter parâmetros chamados por valor de um tipo estrutura ou parâmetros chamados por referência de um tipo estrutura, ou ambos. O programa no Painel 6.1, por exemplo, inclui uma função chamada getDados que possui um parâmetro chamado por referência com o tipo estrutura CDBContaV1. Um tipo estrutura também pode ser o tipo para o valor retornado por uma função. Por exemplo, a definição seguinte é de uma função que toma um argumento de tipo CDBContaV1 e retorna uma estrutura diferente de tipo CDBContaV1. A estrutura retornada terá o mesmo saldo e prazo que o argumento, mas pagará o dobro da taxa de juros que o argumento paga. CDBContaV1 juroDuplo(CDBContaV1 velhaConta) { CDBContaV1 temp; temp = velhaConta; temp.taxaDeJuro = 2*velhaConta.taxaDeJuro; return temp; }
158
Estruturas e Classes
Observe a variável local temp de tipo CDBContaV1; temp é utilizada para construir um valor de estrutura completo da espécie desejada, que é então apresentada como saída pela função. Se minhaConta é uma variável de tipo CDBContaV1 que recebeu valores para suas variáveis-membros, as seguintes linhas fornecerão a suaConta valores para uma conta com o dobro da taxa de juros de minhaConta: CDBContaV1 suaConta; suaConta = juroDuplo(minhaConta); UTILIZE ESTRUTURAS HIERÁRQUICAS
Às vezes é interessante ter estruturas cujos membros são eles mesmos estruturas menores. Por exemplo, um tipo estrutura chamado infoPessoal, que pode ser usado para armazenar altura, peso e data de nascimento de uma pessoa, pode ser definido da seguinte forma: struct Data
{ int dia; int mes; int ano;
}; struct infoPessoa;
{ double altura; //em polegadas int peso; // em libras
Data aniversario; };
Uma variável-estrutura de tipo infoPessoal é declarada da forma usual: infoPessoal pessoa1;
Se a variável-estrutura pessoa1 teve seu valor fixado para registrar a data de nascimento de uma pessoa, o ano em que a pessoa nasceu pode ser exibido na tela da seguinte forma: cout << pessoa1.aniversario.ano;
O modo de ler tais expressões é da esquerda para a direita, e com muito cuidado. Começando da esquerda, pessoa1 é uma variável estrutura de tipo infoPessoal. Para obter a variável-membro com o nome aniversario, utilize o operador ponto, da seguinte forma: pessoa1.aniversario
A variável-membro é ela própria uma variável-estrutura de tipo Data. Assim, essa variável-membro possui variáveis-membros. Uma variável-membro da variável-estrutura pessoa1.aniversario é obtida acrescentando-se um ponto e o nome da variável-membro, como ano, que produz a expressão pessoa1.aniversario exibida acima. No Painel 6.2, reescrevemos a classe para um certificado de depósito bancário do Painel 6.1. Esta nova versão possui uma variável-membro do tipo estrutura Data que abriga a data de vencimento. Também substituímos a variável-membro única saldo por duas novas variáveis-membros que fornecem o saldo inicial e o saldo na data de vencimento.
Painel 6.2
1 2 3
Estrutura com um membro estrutura
(parte 1 de 3)
//Programa para demonstrar o tipo de estrutura CDAccount. #include using namespace std;
4 struct Date 5 { 6 int month; int day; 7 int year; 8 9 };
Esta é a versão aperfeiçoada da estrutura CDAccountV1 definida no Painel 6.1.
10 //Estrutura aperfeiçoada para certificado de depósito bancário: 11 struct CDAccount 12 {
Estruturas
Painel 6.2
Estrutura com um membro estrutura
(parte 2 de 3)
13 double initialBalance; 14 double interestRate; 15 int term;//meses até a data de vencimento 16 Date maturity; //data de vencimento do CDB 17 double balanceAtMaturity; 18 }; 19 20 21 22 23 24 25 26
void getCDData(CDAccount&
theAccount); //Pós-condição: theAccount.initialBalance, theAccount.interestRate, //theAccount.term e theAccount.maturity receberam valores //que o usuário informou ao teclado. void getDate(Date&
theDate); //Pós-condição: theDate.month, theDate.day e theDate.year //receberam valores que o usuário informou ao teclado.
27 int main( ) 28 { 29 CDAccount account; 30 cout << "Informe os dados da conta no dia em que a conta foi aberta:\n"; 31 getCDData(account); 32 double rateFraction, interest; 33 rateFraction = account.interestRate/100.0; 34 interest = account.initialBalance*(rateFraction*(account.term/12.0)); 35 account.balanceAtMaturity = account.initialBalance + interest; 36 37 38 39 40 41 42 43 44 45 }
cout.setf(ios::fixed); cout.setf(ios::showpoint); cout.precision(2); cout << "Na data de vencimento do CDB " << account.maturity.month << "-" << account.maturity.day << "-" << account.maturity.year << endl << "o saldo era de $" << account.balanceAtMaturity << endl; return 0;
46 //utiliza iostream: 47 void getCDData(CDAccount& theAccount) 48 { 49 cout << "Informe o saldo inicial da conta: $"; 50 cin >> theAccount.initialBalance; 51 cout << "Informe a taxa de juros da conta: "; 52 cin >> theAccount.interestRate; 53 cout << "Informe o número de meses até a data de vencimento: "; 54 cin >> theAccount.term; 55 cout << "Informe a data de vencimento:\n"; 56 getDate(theAccount.maturity); 57 } 58 //utiliza iostream: 59 void getDate(Date& theDate) 60 { 61 cout << "Informe o mês: "; 62 cin >> theDate.month; 63 cout << "Informe o dia: "; 64 cin >> theDate.day; 65 cout << "Informe o ano: "; 66 cin >> theDate.year; 67 }
159
160
Estruturas e Classes
Painel 6.2
Estrutura com um membro estrutura
(parte 3 de 3)
DIÁLOGO PROGRAMA-USUÁRIO Informe os dados da conta no dia em que a conta foi aberta: Informe o saldo inicial da conta: $100.00 Informe a taxa de juros da conta: 10.0 Informe o número de meses até a data de vencimento: 6 Informe a data de vencimento: Informe o dia: 14 Informe o mês: 2 Informe o ano: 1899 Quando chegar a data de vencimento do CDB, o saldo será $105.00
■ INICIALIZANDO ESTRUTURAS
Você pode inicializar uma estrutura no momento em que ela e é declarada. Para dar um valor a uma variávelestrutura, coloque depois um sinal de igual e uma lista dos valores-membros entre chaves. Por exemplo, a seguinte definição de uma estrutura para uma data foi apresentada na subseção anterior: struct Data
{ int dia; int mes; int ano;
};
Assim que o tipo Data estiver definido, você pode declarar e inicializar uma variável-estrutura chamada dataPagamento da seguinte forma: Data dataPagamento = {31, 12, 2003};
Os valores inicializantes devem ser dados na ordem que corresponda à ordem das variáveis-membros na definição do tipo estrutura. Neste exemplo, dataPagamento.dia recebe o primeiro valor inicializante, 31, dataPagamento.mes recebe o segundo valor, 12, e dataPagamento.ano recebe o terceiro valor, 2003. Se houver mais valores inicializadores do que membros struct, ocorre um erro. Se houver menos valores inicializadores do que membros struct, os valores fornecidos são usados para inicializar os membros dados, em ordem. Cada membro dado sem um inicializador é inicializado com um valor zero de um tipo apropriado para a variável.
1. Dada a seguin seguinte te estrutura estrutura e declara declaração ção de variáv variável el estrutura, estrutura, struct CDBContaV2 { double saldo; double taxaDeJuro; int prazo; char inicial1; char inicial2; }; CDBContaV2 conta; qual é o tipo de cada uma das seguintes declarações? Assinale todas as que não estiverem corretas. a. conta.saldo b. conta.taxaDeJuro c. CDBContaV1.prazo d. conta.inicial2 e. conta
Estruturas
161
2. Conside Considere re as seguin seguintes tes defin definiçõ ições es de tipo: tipo: struct TipoDeSapato { char estilo; double preco; }; Dadas as definições de tipo estrutura acima, qual deve ser a saída produzida pelo seguinte código? TipoDeSapato sapato1, sapato2; sapato1.estilo = ’A’; sapato1.preco = 9.99; cout << sapato1.estilo << " $" << sapato1.preco << endl; sapato2 = sapato1;
3.
4.
5.
6.
7.
8.
sapato2.preco = sapato2.preco/9; cout << sapato2.estilo << " $" << sapato2.preco << << endl; Qual é o erro na na seguinte seguinte definição definição de de estrutura? estrutura? struct Coisa { int b; int c; } int main( ) { Coisa x; //outro código } Dada Dada a seguint seguintee definiç definição ão struct, struct, struct A { int membro b; int membro c; }; declare que x tem esse tipo estrutura. Inicialize os membros de x, membro b e membro c, com os valores 1 e 2, respectivamente. Aqui está uma uma inicialização inicialização de um tipo tipo estrutura. estrutura. Informe Informe o que acontece com cada cada inicializaçã inicialização. o. Observe quaisquer problemas com essas inicializações. struct Data { int dia; int mes; int ano; }; a. Data dataPagamento = {21, 12}; b. Data dataPagamento = {21, 12, 1995}; c. Data dataPagamento = {21, 12, 19, 95}; Escreva uma definição definição para para um tipo estrutura para para registros formados formados por salário, salário, férias acumulad acumuladas as (um número inteiro de dias) e status (que pode ser horista ou assalariado). Represente o status como um dos dois valores char ’H’ ou ’A ’A’. Chame o tipo de RegistroDoEmpregado. RegistroDoEmpregado . Dê uma definição definição de de função correspon correspondente dente à seguinte seguinte declaração declaração de função. função. (O tipo TipoDeSapato foi dado no Exercício de Autoteste 2.) void leRegistroDeSapato(TipoDeSapato& novoSapato); //Preenche novoSapato com valores lidos a partir do teclado. Dê uma definição definição de de função correspon correspondente dente à seguinte seguinte declaração declaração de função. função. (O tipo TipoDeSapato foi dado no Exercício de Autoteste 2.) TipoDeSapato desconto(TipoDeSapato velhoRegistro); //Retorna uma estrutura que é a mesma de seu argumento, //mas com o preço reduzido em 10%.
162
Estruturas e Classes
Classes
6.2
Todos nós sabemos — o Times sabe —, mas fingimos que não sabemos. Virginia Woolf, Monday or Tuesday
Uma classe é, basicamente, uma estrutura com funções-membros e também dados-membros. As classes são centrais para a metodologia de programação conhecida como programação orientada a objetos . ■
DEFININDO CLASSES E FUNÇÕES-MEMBROS
Uma classe é um tipo similar a um tipo estrutura, mas um tipo classe normalmente possui funções-membros além de variáveis-membros. Um exemplo bastante simples, mas ilustrativo, de uma classe chamada DiaDoAno é dado no Painel 6.3. Esta classe possui uma função-membro chamada saida, além de duas variáveis-membros dia e mes. O termo public: é um especificador de acesso. Quer dizer apenas que não há restrições sobre os membros que se seguem. Falaremos sobre public: e suas alternativas depois de ver este exemplo simples. O tipo DiaDoAno definido no Painel 6.3 é uma definição de classe para objetos cujos valores são datas, como 7 de setembro ou 15 de novembro.
Painel 6.3
Classe com uma função-membro ( parte parte 1 de 2)
1 2 3 4
//Programa para demonstrar um exemplo muito simples da classe. //Uma versão melhor da classe DayOfYear será dada no Painel 6.4. #inc #inclu lude de m> using namespace std;
5 6 7 8 9 10 11
class DayOfYear
{ public : void output( ); int month; int day;
};
Normalmente, as variáveis-membros são private e não public, como neste exemplo. Isso será discutido posteriormente neste capítulo.
Declaração de função-membro
12 int main( ) 13 { 14 DayOfYear DayOfY ear tod today, ay, birt birthday hday;; 15 coutt << "In cou "Infor forme me a dat dataa de hoj hoje:\ e:\n"; n"; 16 coutt << "Infor cou "Informe me o dia do mês mês:: "; 17 ci cinn >> to today day.m .mont onth; h; 18 coutt << "In cou "Infor forme me o mês com um núm número: ero: "; 19 cin >> to toda day. y.da day; y; 20 coutt << "In cou "Infor forme me o dia do seu ani anivers versário ário:\n" :\n";; 21 coutt << "In cou "Infor forme me o mês com um núm número: ero: "; 22 ci cinn >> bi birth rthda day.m y.mont onth; h; 23 coutt << "In cou "Infor forme me o dia do mês mês:: "; 24 ci cinn >> bi birth rthda day.d y.day; ay; 25 26 27 28 29 30
cou outt << "H "Hoj ojee é "; to toda day. y.ou outp tput ut(( ); cout out << endl; ndl; coutt << "O seu ani cou aniver versári sárioo é"; birth bir thday day.ou .outp tput( ut( ); cout out << endl; ndl;
31 32 33 34
if (today.month == birthday.month && today.day == birthday.day)
coutt << "Fe cou "Feliz liz Ani Aniver versári sário!\n o!\n"; "; else
coutt << "Fe cou "Feliz liz Dia de Não Não-Ani -Anivers versário ário!\n" !\n";;
Chama a função-membro output
Classes
Painel 6.3
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
163
Classe com uma função-membro ( parte parte 2 de 2)
return 0; } //Utiliza iostream: void DayOfYear::output( ) { switch (month) { case 1: co cout ut << "J "Jane aneiro iro "; br break eak;; case 2: co cout ut << "F "Feve everei reiro ro "; bre break; ak; case 3: co cout ut << "M "Març arçoo "; bre break; ak; case 4: Definição da função-membro co cout ut << "A "Abri brill "; bre break; ak; case 5: co cout ut << "M "Maio aio "; bre break ak;; case 6: co cout ut << "J "Junh unhoo "; bre break; ak; case 7: co cout ut << "J "Julh ulhoo "; bre break; ak; case 8: co cout ut << "A "Agos gosto to "; br break eak;; case 9: co cout ut << "S "Sete etembr mbroo "; bre break ak;; case 10: co cout ut << "O "Outu utubro bro "; br break eak;; case 11: co cout ut << "N "Nove ovembr mbroo "; bre break ak;; case 12: co cout ut << "D "Deze ezembr mbroo "; bre break ak;; default : cout << "Erro em DayOfYea DayOfYear::outp r::output. ut. Entre em contato com o fornec fornecedor edor do software software."; ."; }
cout out << day; }
DIÁLOGO PROGRAMA-USUÁRIO Informe a data de hoje: Informe o dia do mês: 15 Informe o mês com um número: 10 Informe o dia do seu aniversário: Informe o dia do mês: 21 Informe o mês com um número: 2 Hoje é 15 outubro O seu aniversário é em 21 fevereiro Feliz Dia de Não-Aniversário!
O valor de uma variável de um tipo classe é chamado de objeto objeto (quando se fala de maneira imprecisa, uma variável de um tipo classe também costuma ser chamada de objeto ). ). Um objeto possui tanto membros dados como membros funções. Quando se programa com classes, um programa é encarado como uma coleção de objetos interagindo. Os objetos podem interagir porque são capazes de ações, ou seja, invocações de funções-membros. As variáveis de um tipo classe guardam objetos como valores. As variáveis de um tipo classe são declaradas da mesma forma que as variáveis de tipos predefinidos e que as variáveis-estruturas.
164
Estruturas e Classes
Por enquanto, ignore a palavra public: exibida no Painel 6.3. O resto da definição da classe DiaDoAno é muito semelhante a uma definição de estrutura, a não ser pelo fato de que utiliza a palavra-chave class em vez de struct e de que lista a função-membro saida (assim como as variáveis-membros dia e mes). Observe que a função-membro saida é listada dando-se sua declaração (protótipo). Uma definição de classe normalmente contém apenas a declaração para suas funções-membros. As definições para as funções-membros normalmente são dadas em outro lugar. Em uma definição de classe em C++, você pode mesclar a ordem das variáveis-membros e funções-membros da forma que desejar, mas o estilo que nós seguimos tende a listar as funções-membros antes das variáveis-membros. variáveis-membros. Variáveis-membros Variáveis-membros para um objeto de um tipo classe são especificadas por meio do operador ponto, da mesma forma que o operador ponto é utilizado para especificar variáveis-membros de uma estrutura. Por exemplo, se hoje é uma variável do tipo classe DiaDoAno definida no Painel 6.3, então hoje.dia e hoje.mes são as duas variá veis-membros do objeto hoje. Funções-membros para classes que você define são invocadas por meio do operador ponto de uma forma similar àquela pela qual se especifica uma variável-membro. Por exemplo, o programa no Painel 6.3 declara dois objetos de tipo DiaDoAno da seguinte forma: DiaDoAno hoje, aniversario;
A função-membro saida é chamada com o objeto hoje da seguinte forma: hoje.saida( );
e a função-membro saida é chamada com o objeto aniversario assim: aniversario.saida( );
Quando uma função-membro é definida, a definição deve incluir o nome da classe, porque pode haver duas ou mais classes que possuam funções-membros com o mesmo nome. No Painel 6.3, há apenas uma definição de classe, mas em outras situações pode-se ter muitas definições de classe e mais de uma classe pode ter funçõesmembros com o mesmo nome. A definição para a função-membro saida da classe DiaDoAno é mostrada na parte 2 do Painel 6.3. A definição é semelhante a uma definição de função comum, a não ser pelo fato de que é preciso especificar o nome da classe no cabeçalho da definição de função. O cabeçalho da definição de função para a função-membro saida é assim: void DiaDoAno::saida(
)
O operador :: é chamado operador de resolução de escopo e serve a um propósito semelhante ao do operador ponto. Tanto o operador ponto quanto o operador de resolução de escopo são utilizados para dizer de que uma função-membro é membro. Entretanto, o operador de resolução de escopo :: é utilizado com um nome de classe, enquanto o operador ponto é utilizado com objetos (ou seja, com variáveis de classe). O operador de resolução de escopo geralmente é chamado de qualificador de tipo, porque ele especializa ("qualifica") o nome da função como um tipo particular. Veja a definição da função-membro DiaDoAno:: saida fornecida no Painel 6.3. Observe que, na definição de função de DiaDoAno::saida, nós utilizamos os membros de estrutura dia e mes sozinhos, sem primeiro fornecer o objeto e o operador ponto. Isso não é tão estranho quanto possa parecer de início. A esta altura vamos apenas definir a função-membro saida. Essa definição de saida aplicar-se-á a todos os objetos de tipo DiaDoAno, mas a essa altura não sabemos os nomes dos objetos de tipo DiaDoAno que utilizaremos, então não podemos fornecer seus nomes. Quando a função-membro é chamada, como em hoje.saida( );
todos os nomes dos membros na definição de função são especializados com o nome do objeto que faz a chamada. Assim, a função acima é equivalente a: { switch (hoje.mes)
{ case 1:
Classes
165
. . . } cout << hoje.dia; }
DEFINIÇÃO DE FUNÇÃO-MEMBRO Uma função-membro é definida de maneira similar a qualquer outra função, a não ser pelo fato de que Nome_Classe e o operador de resolução de escopo, ::, são fornecidos no cabeçalho da função.
SINTAXE Tipo_Retornado Nome_Classe::Nome_Funcao(Lista_Parametros) {
Comandos_Corpo_Função }
EXEMPLO Veja o Painel 6.3. Observe que as variáveis-membros (dia e mes ) não são precedidas por um nome de objeto e ponto quando ocorrem em uma definição de função-membro. Na definição de função para uma função-membro, podem-se usar os nomes de todos os membros dessa classe (tanto os membros dados como os membros funções) sem utilizar o operador ponto.
OPERADOR PONTO E OPERADOR DE RESOLUÇÃO DE ESCOPO Tanto o operador ponto quanto o operador de resolução de escopo são usados com membros de estrutura para especificar de que eles são membros. Por exemplo, suponha que você tenha declarado uma classe chamada DiaDoAno e você declare um objeto chamado hoje da seguinte forma: DiaDoAno hoje;
Emprega-se o operador ponto para especificar um membro do objeto hoje. Por exemplo, saida é uma função-membro da classe DiaDoAno (definida no Painel 6.3) e a seguinte chamada de função produzirá como saída os valores dados armazenados no ob jeto hoje. hoje.saida( );
Emprega-se o operador de resolução de escopo, ::, para especificar o nome da classe quando se fornece a definição de função para uma função-membro. Por exemplo, o cabeçalho da definição de função para a função membro saida seria assim: void DiaDoAno::saida( )
Lembre-se de que o operador de resolução de escopo, ::, é utilizado com um nome de classe, enquanto o operador ponto é utilizado com um objeto daquela classe.
UMA CLASSE É UM TIPO COMPLETO Uma classe é um tipo exatamente como os tipos int e double. Pode-se ter variáveis de um tipo classe, pode-se ter parâmetros de um tipo classe, uma função pode retornar um valor de um tipo classe e, de forma geral, pode-se usar um tipo classe como qualquer outro tipo.
9. A seguir, seguir, temo temoss uma defi definiç nição ão da clas classe se DiaDoAno do Painel 6.3 de modo que, agora, há uma funçãoentrada membro adicional chamada . Escreva uma definição apropriada para a função-membro entrada. class DiaDoAno
{ public: void entrada( ); void saida( ); int dia; int mes;
};
166
Estruturas e Classes
10. Dada a seguinte seguinte definição definição de classe, escreva escreva uma definição definição apropriada apropriada para para a função-membro função-membro set. class Temperatura { public: void set(double novosGraus, char novaEscala); //Fixa as variáveis-membros para os valores dados como //argumentos. double graus; char escala; //’F’ para Fahrenheit ou ’C’ para Celsius. }; 11. Com cuidado, cuidado, estabeleça estabeleça a distinção distinção entre entre o significado significado e o uso do operador operador ponto ponto e do operador de resolução de escopo, ::.
ENCAPSULAMENTO Um tipo de dados, como o tipo int, possui certos valores especificados, como 0, 1, -1, 2, e assim por diante. Tendemos a pensar no tipo de dados como sendo esses valores, mas as operações sobre esses valores são tão importantes quanto os valores. Sem as operações, não se pode fazer nada de interessante com esses valores. As operações para o tipo int são +, -, *, /, % e mais alguns poucos operadores e funções de biblioteca predefinida. Não se deve pensar no tipo de dados como sendo apenas uma coleção de valores. Um tipo de dados consiste em uma coleção de valores associada a um conjunto de operações básicas definidas sobre esses valores. Um tipo de dados é ADT) se os programadores que usam o tipo não têm acesso aos detalhes chamado de um tipo de dados abstrato ( ADT de como valores e operações são implementados. Os tipos predefinidos, como int, são tipos dados abstratos (ADTs). Não se sabe como as operações, como as de + e as de *, são implementadas para o tipo int. Mesmo que se saiba, não se pode utilizar essa informação em nenhum programa em C++. As classes, que são tipos definidos pelo programador, também devem ser ADTs, ou seja, os detalhes de como as "operações" são implementadas de vem ser ocultos de qualquer programador que os utilize ou, pelo menos, irrelevantes para ele. As operações de uma classe são as funções-membros (públicas) da classe. Um programador que utiliza uma classe não deve precisar ver as definições das funções-membros. funções-membros. As declarações de funções-membros, funções-membros, dadas na definição de classe, e uns poucos comentários devem ser tudo de que o programador precisa para utilizar a classe. Um programador que utiliza uma classe também não deve precisar saber como os dados da classe são implementados. A implementação dos dados deve ser oculta como a implementação das funções-membros. Na verdade, é quase impossível distinguir entre ocultar a implementação das funções-membros e a implementação dos dados. Para um programador, a classe DiaDoAno (Painel 6.3) possui datas como dados, não números. O programador não deve saber ou se preocupar se o mês de março é implementado como o valor int 3, a string "Março" ou de alguma outra forma. A definição de uma classe de modo que a implementação das funções-membros e dos dados nos objetos não seja conhecida do programador que utiliza a classe, ou, pelo menos, seja irrelevante para ele, é conhecida por di versos termos. Os termos utilizados mais comuns são ocultação de informação , abstração de dados ou encapsulamento, termos que significam que os detalhes da implementação de uma classe são ocultados do programador que utiliza a classe. Esse princípio é um dos maiores dogmas da programação orientada a objetos (OOP). Quando se fala de OOP, o termo usado com mais freqüência é encapsulamento . Uma das formas de se aplicar esse princípio às suas definições de classe é tornar todas as variáveis-membros privadas, assunto de que trataremos na próxima subseção. ■
MEMBROS PÚBLICOS E PRIVADOS Veja novamente a definição do tipo DiaDoAno, dada no Painel 6.3. A fim de utilizar essa classe, você precisa saber que existem duas variáveis-membros de tipo int que se chamam dia e mes. Isso viola o princípio do encapsulamento (ocultação da informação) que abordamos na subseção anterior. O Painel 6.4 é uma versão reescrita da classe DiaDoAno que se conforma melhor a esse princípio do encapsulamento. ■
Classes
167
Observe as palavras private: e public: no Painel 6.4. Diz-se que todos os itens que seguem a palavra private: (nesse caso, as variáveis membros dia e mes) são privados, o que significa que eles não podem ser referenciados por nomes em nenhum lugar, exceto dentro das definições das funções-membros da classe DiaDoAno. Por exemplo, com essa definição alterada da classe DiaDoAno, as duas atribuições seguintes e os outros códigos indicados não são mais permitidos na função main do programa nem em qualquer outra definição de função, exceto as funções-membros da classe DiaDoAno. DiaDoAno hoje; //Esta linha está OK. hoje.dia = 25; //ILEGAL hoje.mes = 12; //ILEGAL cout << hoje.dia; // ILEGAL cout << hoje.mes; //ILEGAL if (hoje.mes == 1) //ILEGAL cout << "Janeiro";
Assim que uma variável-membro variável-membro se torna uma variável-membro variável-membro privada, não há mais como alterar seu valor (ou fazer referência à variável-membro de qualquer forma) a não ser utilizando uma das funções-membros. Isso quer dizer que o compilador imporá a ocultação da implementação dos dados para a classe DiaDoAno. Se você olhar Painel 6.4
Classe com membros privados (parte 1 de 3)
1 2 3
#inc #inclu lude de m> #incl nclude using namespace std;
4 5 6 7 8 9 10
class DayOfYear
Esta é uma versão aperfeiçoada da classe DayOfYear que fornecemos no Painel 6.3.
{ public: void input( ); void output( ); void set(int newMonth, int newDay);
11 12 13
//Pré-condição: newMonth e newDay formam uma data possível. void set(int newMonth);
//Pré-condição: 1 <= newMonth <= 12 //Pós-condição: A data é fixada para o primeiro dia do mês dado.
14 int getMonthNumber( ); //Retorna 1 para janeiro, 2 para fevereiro, etc. 15 int getDay( ); 16 private: 17 int month; Membros privados 18 int day; 19 }; 20 int main( ) 21 { 22 DayOfYear DayOfY ear tod today, ay, bach bachBirt Birthday hday;; 23 coutt << "In cou "Infor forme me a dat dataa de hoj hoje:\ e:\n"; n"; 24 tod oday ay.i .inp nput ut(( ); 25 co cout ut << "A da data ta de ho hoje je é" é";; 26 tod oday ay.o .out utpu put( t( ); 27 cout out << endl; ndl; 28 29 30 31 32 33 34
ba bachB chBirt irthd hday. ay.set set(3 (3,, 21 21); ); coutt << "O ani cou aniver versári sárioo de J. S. é"; ba bachB chBirt irthd hday. ay.out outpu put( t( ); cout out << endl; ndl; if ( today.getMonthNumber( ) == bachBirthday.getMonthNumber( ) && today. tod ay.getD getDay( ay( ) == bac bachBir hBirthda thday.ge y.getDay tDay(( ) ) cout << "Feli "Felizz Aniver Aniversário, sário, Johan Johannn Sebast Sebastian!\n"; ian!\n";
168
Estruturas e Classes
Painel 6.4
35 36 37 38 39 }
Classe com membros privados (parte 2 de 3)
else
cout << "Feli "Felizz Aniver Aniversário, sário, Johan Johannn Sebast Sebastian!\n"; ian!\n"; Observe que o nome da função set está sobrecarregado. Pode-se sobrecarregar uma função-membro exatamente como se sobrecarr sobrecarrega ega qualquer outra função.
return 0;
40 //Utiliza iostream e cstdlib: 41 void DayOfYear::set(int newMonth, int newDay) 42 { 43 if ((newMonth >= 1) && (newMonth <= 12)) 44 mon onth th = ne newM wMon onth th;; 45 else 46 { 47 cout << "Valo "Valorr ilegal para o mês! Program Programaa aborta abortado.\n"; do.\n"; 48 exit(1); 49 } 50 if ((newDay >= 1) && (newDay <= 31)) 51 day = new newDay; Day; 52 else 53 { 54 cout << "Valo "Valorr ilegal para o dia! Program Programaa aborta abortado.\n"; do.\n"; 55 exit(1); 56 } 57 } 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
Funções mutantes
//Utiliza iostream e cstdlib: void DayOfYear::set(int newMonth)
{ if ((newMonth
>= 1) && (newMonth <= 12)) mon onth th = ne newM wMon onth th;;
else
{ cout << "Valo "Valorr ilegal para o mês! Program Programaa aborta abortado.\n"; do.\n"; exit(1); } day = 1; } int DayOfYear::getMonthNumber(
)
Funções de acesso
{ return month;
}
75 int DayOfYear::getDay( ) 76 { 77 return day; 78 } 79 //Utiliza iostream e cstdlib: 80 void DayOfYear::input( ) 81 { 82 coutt << "In cou "Infor forme me o mês com um núm número: ero: "; 83 cin >> month; 84 coutt << "In cou "Infor forme me o dia do mês mês:: "; 85 cin >> day; 86 if ((month < 1) || (month > 12) || (day < 1) || (day > 31)) 87 { 88 coutt << "Da cou "Data ta ilegal! ilegal! Pro Program gramaa abo aborta rtado.\ do.\n"; n";
Os membros privados podem ser utilizados em definições de funçõesmembros (mas não em outros lugares).
Classes
Painel 6.4 89 90 91 }
169
Classe com membros privados (parte 3 de 3) exit(1);
}
92 void DayOfYear::output( ) 93
DIÁLOGO PROGRAMA-USUÁRIO Informe a data de hoje: Informe o dia do mês: 21 Informe o mês com um número: 3 A data de hoje é 21 de março O aniversário de J. S. Bach é em 21 de março Feliz Aniversário, Johann Sebastian!
com cuidado para o programa no Painel 6.4, verá que o único lugar em que os nomes de variáveis-membros dia e mes são usados é na definição das funções-membros. Não há referência a hoje.dia, hoje.mes, bachAniversario.dia ou bachAniversario.mes fora das definições de funções-membros. Todos os itens que seguem a palavra public: (nesse caso as funções-membros) são chamados de públicos, o que significa que podem ser referenciados por nome em qualquer lugar. Não há restrições sobre o uso de membros públicos. Quaisquer variáveis-membros podem ser públicas ou privadas. Quaisquer funções-membros podem ser públicas ou privadas. Entretanto, a prática normal da boa programação requer que todas as as variáveis-membros sejam privadas e que a maioria das funções-membros sejam públicas. Pode-se ter qualquer número de ocorrências dos especificadores de acesso public e private em uma definição de classe. Cada vez que você inserir a legenda public:
a lista de membros muda de privada para pública. Cada vez que você inserir a legenda private:
a lista de membros volta a ser privada. Você não precisa ter apenas um grupo de membros público e um privado. Entretanto, é comum ter apenas uma seção pública e uma seção privada. Não existe um entendimento universal sobre se são os membros públicos ou os privados que devem ser listados primeiro. A maioria parece preferir listar os membros públicos primeiro. Isso permite uma fácil visualização das porções que os programadores que utilizam a classe utilizam realmente. Você pode fazer sua própria opção quanto ao que deseja colocar primeiro, mas os exemplos no livro seguem a opinião da maioria e listam os membros públicos antes dos privados. Em certo sentido, o C++ parece favorecer a colocação dos membros privados primeiro. Se o primeiro grupo de membros não possuir o especificador public: nem private:, os membros desse grupo automaticamente serão privados. Você verá esse comportamento-padrão utilizado em código e deve se familiarizar com ele. Entretanto, não o utilizaremos neste livro. ■
FUNÇÕES DE DE ACESSO: ACESSOR ACESSOR (get) e MUTATOR MUTATOR (set)
Você sempre deve tornar privadas todas as variáveis-membros variáveis-membros de uma classe. Às vezes, no entanto, você pode precisar fazer algo com os dados em um objeto de uma classe. As funções-membros permitirão que se façam muitas coisas com os dados em um objeto, porém, mais cedo ou mais tarde, você vai querer ou precisar fazer algo com os dados para os quais não há função-membro. Como se pode fazer algo novo com os dados em um objeto? A resposta é que você pode fazer qualquer coisa razoável que deseje, desde que equipe suas classes com funções acessor e mutator adequadas. Essas funções-membros permitem que você tenha acesso aos dados em um objeto e os altere de forma bastante geral. As funções acessor permitem permitem que os dados sejam lidos. No Painel 6.4, as fun-
170
Estruturas e Classes
ções-membros getDia e getNumeroMes são funções acessor. As funções acessor não precisam retornar literalmente os valores de cada variável-membro, mas precisam retornar algo equivalente a esses valores. Por exemplo, para uma classe como DiaDoAno, você pode ter uma função acessor que retorne o nome do mês como algum tipo de valor em string, em vez de retornar o mês como um número. As funções mutator permitem permitem que se alterem os dados. No Painel 6.4, as duas funções chamadas set são funções mutator. Faz parte da tradição utilizar nomes que incluam a palavra get para funções acessor e nomes que incluam a palavra set para funções mutator. (As funções entrada e saida no Painel 6.4 são, na realidade, funções mutator e acessor, respectivamente, mas E/S é um caso tão especial que elas geralmente são chamadas de funções E/S em em vez de funções acessor e mutator.) Suas definições de classe devem sempre fornecer uma coleção adequada de funções acessor e mutator. Pode parecer que as funções acessor e mutator destruam o objetivo de tornar as variáveis-membros privadas, mas isso não acontece. Observe a função mutator set no Painel 6.4. Ela não permitirá que você fixe a variávelmembro dia como 13 ou qualquer número que não esteja no intervalo de 1 a 31 (inclusive). De forma similar, ela não permitirá que você fixe a variável membro mes como 13 ou qualquer outro número que não represente um mês. Se as variáveis fossem públicas você poderia fixar a data em valores absurdos para uma data. (Dessa forma, você ainda pode fixar valores que não representam uma data real, como 31 de fevereiro, mas seria fácil excluir essas datas. Não as excluímos para manter o exemplo simples.) Com as funções mutator, você pode controlar e filtrar as mudanças nos dados.
12. Suponha Suponha que seu programa programa contenha contenha a seguinte seguinte definição definição de classe: classe: class Automovel
{ public: void setPreco(double novoPreco); void setRendimento(double novoRendimento); double getPreco( ); private: double preco; double rendimento; double getRendimento( );
};
e suponha que a função main do seu programa contenha a seguinte declaração e que o programa fixe de algum modo os valores de todas as variáveis-membros com alguns valores: Automovel hyundai, jaguar;
Quais dos seguintes comandos são, então, permitidos na função main do seu programa? hyundai.preco = 4999.99; jaguar.setPreco(30000.97); double umPreco, umRendimento; umPreco = jaguar.getPreco( ); umPreco = jaguar.getRendimento( ); umRendimento = hyundai.getRendimento( ); hyundai = jaguar;
13. Suponha Suponha que você mude mude o Exercício Exercício de Autoteste Autoteste 12 de forma forma que, na definiçã definição o da classe classe Automovel, todas as variáveis-membros sejam públicas em vez de privadas. Como isso alteraria sua resposta? 14. 14. Expl Expliq ique ue o que que public: e private: significam em uma definição de classe. 15. a. Quan Quanta tass seçõ seções es public: são necessárias em uma classe para a classe ser útil? b. Quantas seções private: são necessárias em uma classe?
Classes
171
INTERFACE E IMPLEMENTAÇÃO SEPARADAS O princípio do encapsulamento diz que você deve definir as classes de tal forma que um programador que utilize uma classe não precise se preocupar com detalhes de como ela é implementada. O programador que utiliza a classe só precisa conhecer as regras de como utilizá-la. Essas regras são conhecidas como interface ou API. Há algumas discordâncias sobre o que exatamente as iniciais API significam, mas em geral se considera que se refiram a algo como interface de aplicação com o programador (application programmer interface) ou interface abstrata de programação (abstract programming interface) ou algo similar. Neste livro, chamaremos essas regras de interface da classe. É importante não se esquecer de que há uma clara distinção entre a interface e a implementação de uma classe. Se sua classe é bem projetada, qualquer programador que a utiliza precisa conhecer apenas a interface da classe e não precisa conhecer nenhum detalhe da implementação da classe. Uma classe cuja interface e implementação são separadas dessa forma, às vezes, é chamada de tipo de dados abstrato (ADT) ou uma classe bem encapsulada. No Capítulo 11 mostraremos como separar a interface e a implementação, colocando-as em arquivos diferentes, mas o importante é mantê-las separadas conceitualmente. Para uma classe C++, a interface consiste em duas espécies: os comentários, normalmente no início da definição de classe, que dizem o que os dados do objeto devem representar, como uma data, uma conta bancária ou uma simulação de lava-carros; e as funções-membros públicas da classe com os comentários que dizem como utilizar essas funções-membros públicas. Em uma classe bem projetada, a interface da classe deve ser tudo o que é necessário saber a fim de utilizar a classe em seu programa. A implementação de uma classe diz como a interface da classe é concretizada em código C++. A implementação consiste nos membros privados da classe e nas definições das funções-membros tanto públicas quanto privadas. Embora a implementação seja necessária a fim de executar um programa que utiliza a classe, você não precisa saber nada sobre a implementação para escrever o resto de um programa que utiliza a classe; ou seja, você não precisa saber nada sobre a implementação a fim de escrever a função main do programa e escrever quaisquer funções não-membros ou outras classes utilizadas pela função main. A vantagem mais óbvia que advém da nítida separação da interface e da implementação de suas classes é que se pode alterar a implementação sem ter de alterar outras partes do programa. Em grandes projetos de programação essa divisão entre a interface e a implementação facilita a divisão do trabalho entre vários programadores. Se a interface for bem projetada, um programador pode escrever a implementação para a classe enquanto outros programadores escrevem o código que utiliza a classe. Mesmo que você seja o único programador trabalhando em um projeto, você tem uma tarefa maior dividida em tarefas menores, o que torna seu programa mais fácil de projetar e depurar.
TESTE PARA ENCAPSULAMENTO Se sua definição de classe produzir um ADT (ou seja, caso separe adequadamente a interface e a implementação), você pode mudar a implementação da classe (isto é, alterar a representação dos dados e/ou alterar a implementação de algumas funções-membros) sem precisar alterar mais nenhum código para qualquer programa que utilize a definição de classe. Eis aqui um teste seguro para verificar se você definiu um ADT ou uma classe que não está encapsulada adequadamente. Por exemplo, você pode alterar a implementação da classe DiaDoAno no Painel 6.4 para a seguinte e nenhum programa que utilizar esta definição de classe precisará de qualquer alteração: class DiaDoAno
{ public: void entrada( ); void saida( ); void set(int novoDia, int novoMes);
//Pré-condição: novoDia e novoMes formam uma data possível. //Pós-condição: A data é refixada de acordo com os argumentos. void set(int novoMes);
//Pré-condição: 1 <= novoMes <= 12 //Pós-condição: A data é fixada para o primeiro dia do mês. int getNumeroMes( );
//Retorna 1 para janeiro, 2 para fevereiro, etc. int getDia( );
172
Estruturas e Classes
private: char primeiraLetra;//do mês char segundaLetra;//do mês char terceiraLetra;//do mês int dia;
}; Nessa versão, um mês é representado pelas primeiras três letras em seu nome, como ’j’, ’a’ e ’n’ para janeiro. As funções-membros também devem ser reescritas, é claro, mas podem ser reescritas e se comportarem exatamente como antes. Por exemplo, a definição da função getNumeroMes poderia começar assim: int DiaDoAno::getNumeroMes( ) { if (primeiraLetra == ’j’ && segundaLetra == ’a’ & terceiraLetra == ’n’) return 1; if (segundaLetra == ’f’ && segundaLetra == ’e’ & terceiraLetra == ’v’) return 2; . . . Isso seria bastante entediante, mas nada difícil.
■ ESTRUTURAS VERSUS CLASSES
As estruturas normalmente são usadas com todas as variáveis-membros públicas e não com funções-membros. Entretanto, em C++, uma estrutura pode ter variáveis-membros privadas e tanto funções-membros públicas quanto privadas. Exceto por algumas diferenças de notação, uma estrutura em C++ pode fazer qualquer coisa que uma classe pode. Agora que dissemos tudo isso e satisfizemos nosso compromisso em dizer toda a verdade, nós o aconselhamos a esquecer esse detalhe técnico a respeito das estruturas. Se você levar esse detalhe técnico a sério e utilizar CLASSES E OBJETOS
Uma classe é um tipo cujas variáveis podem ser tanto variáveis-membros quanto funções-membros. A sintaxe para uma definição de classe é dada abaixo. SINTAXE
class Nome_Classe
{ . public:
Especificacao_MembroN+1 Especificacao_MembroN+2 . . .
Membros públicos
private:
Especificacao_Membro_1 Especificacao_Membro_2 . . . Especificacao_MembroN
};
Membros privados
Não esqueça este ponto-e-vírgula.
Cada Especificacao_Membro_1 é ou uma declaração de variável-membro ou uma declaração de função-membro (protótipo). Seções adicionais public: e private: são permitidas. Se o primeiro grupo de membros não possui um rótulo public: ou private:, então é o mesmo que se houvesse um private: antes do primeiro grupo. EXEMPLO
class Bicicleta
{
Resumo do Capítulo
173
public: char getCor( ); int numeroDeMarchas( ); void set(int asMarchas, char aCor); private: int marchas; char cor;
};
Uma vez que uma classe é definida, uma variável objeto (variável do tipo classe) pode ser declarada da mesma forma que variá veis de qualquer outro tipo. Por exemplo, a linha seguinte declara duas variáveis objeto do tipo Bicicleta: Bicicleta minhaBicicleta, suaBicicleta;
estruturas da mesma forma que utiliza classes, terá dois nomes (com diferentes regras de sintaxe) para o mesmo conceito. Por outro lado, se você usar as estruturas como nós as descrevemos, terá uma diferença significativa entre estruturas (como você as usa) e classes, e seu uso será o mesmo que o da maioria dos outros programas. Uma diferença entre uma estrutura e uma classe é no modo como tratam um grupo inicial de membros que não possuem especificador de acesso público nem privado. Se o primeiro grupo de membros em uma definição não possui rótulo public: nem private:, uma estrutura presume que o grupo seja público, enquanto uma classe presumiria que o grupo fosse privado. PENSANDO OBJETOS
Se você nunca programou com classes, pode levar algum tempo até captar a sensação de programar com elas. Quando se programa com classes, o centro do palco é ocupado pelos dados e não pelos algoritmos. Não é que não existam algoritmos. Entretanto, os algoritmos são feitos para se adaptarem aos dados, o que é o contrário de projetar os dados para se adaptarem ao algoritmo. É uma diferença de ponto de vista. No caso extremo, que muitos consideram o melhor estilo, não se tem funções globais, apenas classes com funções-membros. Nesse caso, você define os objetos e como os objetos interagem, em vez de algoritmos que atuam sobre dados. Discutiremos os detalhes de como fazer isso no decorrer do livro. É claro que você pode ignorar as classes completamente ou relegá-las a um papel secundário, mas nesse caso você estará programando em C, não em C++.
16. Quando você define uma classe em C++, deve tornar as variáveis-membros públicas ou privadas? Deve tornar as funções-membros públicas ou privadas? 17. Quando você define uma classe em C++, que itens são considerados parte da interface? Que itens são considerados parte da implementação?
■
■ ■
■
■ ■
Uma estrutura pode ser usada para combinar dados de diferentes tipos em um único (composto) valor de dados. Uma classe pode ser usada para combinar dados e funções em um único (composto) objeto. Uma variável-membro ou uma função-membro de uma classe pode ser pública ou privada. Se for pública, pode ser usada fora da classe. Se for privada, só pode ser usada na definição de uma função-membro. Uma função pode ter parâmetros formais de um tipo classe ou estrutura. Uma função pode retornar valores de um tipo classe ou estrutura. Uma função-membro de uma classe pode ser sobrecarregada da mesma forma que as funções comuns. Quando se define uma classe em C++, deve-se separar a interface e a implementação, de forma que qualquer programador que utiliza a classe precise conhecer apenas a interface e não precise sequer olhar para a implementação. Este é o princípio do encapsulamento.
174
Estruturas e Classes
RESPOSTAS DOS EXERCÍCIOS DE AUTOTESTE 1. a. double b. double c. ilegal — não se pode utilizar um identificador de estrutura em vez de uma variável-estrutura. d. char e. CDBContaV2 2. A $9.99 A $1.11
3. Está faltando um ponto-e-vírgula no final da definição de Coisa. 4. A x = {1,2}; 5. a. Inicializadores a menos; não é um erro de sintaxe. Depois da inicialização, dia == 21, mes == 12 e ano == 0. As variáveis-membros que não receberam um inicializador são inicializadas como um zero do tipo apropriado. b. Correto após inicialização. 21 == dia, 12 == mes e 1995 == ano. c. Erro: inicializadores demais. 6. struct RegistroDoEmpregado { double salario; int ferias; char status;
};
7. void leRegistroDeSapato(TipoDeSapato& novoSapato) { cout << "Informe o estilo do sapato (uma letra): "; cin >> novoSapato.estilo; cout << "Informe preço do sapato $"; cin << novoSapato.preco; } 8. TipoDeSapato desconto(TipoDeSapato velhoRegistro) { TipoDeSapato temp; temp.estilo = velhoRegistro.estilo; temp.preco = 0.90*velhoRegistro.preco; return temp; } 9. void DiaDoAno::entrada( ) { cout << "Informe o dia do mês: "; cin >> dia; cout << "Informe o mês com um número: "; cin >> mes; } 10. void Temperatura::set(double novosGraus, char novaEscala) { graus = novosGraus; escala = novaEscala; }
11. Tanto o operador ponto quanto o operador de resolução de escopo são usados com membros de estrutura para especificar de que classe ou estrutura o nome de membro é um membro. Se a classe DiaDoAno for definida como no Painel 6.3 e hoje for um objeto da classe DiaDoAno, pode-se ter acesso ao membro mes por meio do operador ponto: hoje.mes. Quando damos a definição de uma função-membro, o operador de resolução de escopo é usado para dizer ao compilador que essa função é aquela declarada na classe. 12. hyundai.preco = 4999.99; //ILEGAL. preço é privado jaguar.setPreco(30000.97); //LEGAL
Projetos de Programação
175
double umPreco,
umRendimento; //LEGAL umPreco = jaguar.getPreco( ); //LEGAL umPreco = jaguar.getRendimento( ); //ILEGAL. getRendimento é //privado. umRendimento = hyundai.getRendimento( ); //ILEGAL. getRendimento é //privado. hyundai = jaguar; //LEGAL
13. Após a mudança, todos devem ser legais. 14. A todos os membros (variáveis-membros e funções-membros) que são assinalados como private: só se pode ter acesso por nome nas definições de funções-membros (tanto públicas quanto privadas) da mesma classe. Em relação aos membros assinalados como public:, não há restrições quanto ao local onde podem ser usados. 15. a. Só uma. O compilador avisa se você não tiver membros public: em uma classe (ou struct). b. Nenhuma, mas normalmente esperamos encontrar pelo menos uma seção private: em uma classe. 16. Todas as variáveis-membros devem ser privadas. As funções-membros que são parte da interface devem ser públicas. Você também pode ter funções auxiliares que só são usadas na definição de outras funções-membros. Essas funções auxiliares devem ser privadas. 17. Todas as declarações de variáveis-membros privadas são parte da implementação. (Não deve haver variá veis-membros públicas.) Todas as declarações para funções-membros públicas da classe (que são listadas nas definições de classe), assim como os comentários que explicam essas declarações, fazem parte da interface. Todas as declarações de funções-membros privadas fazem parte da implementação. Todas as definições de funções-membros (quer a função seja pública, quer privada) fazem parte da implementação.
PROJETOS DE PROGRAMAÇÃO 1. Escreva um programa de notas para uma classe com as seguintes regras: a. Existem duas provas, cada uma com nota máxima 10. b. Existe um exame no meio do ano e um final, cada um com nota máxima 100. c. O exame final vale 50% da nota final, o de meio de ano vale 25% e as duas provas juntas valem um total de 25%. (Não se esqueça de normalizar as notas das provas. Elas devem ser convertidas em uma porcentagem antes de se fazer a média.) Qualquer nota de 90 ou mais equivale a A; entre 80 e 90, B; entre 70 e 80, C; entre 60 e 70, D; e menos de 60, E. O programa lerá as notas dos alunos e apresentará como saída o boletim do estudante, que consiste nas duas provas e nos dois exames, além da média numérica de todo o curso e da letra final. Defina e utilize uma estrutura para o boletim do aluno. 2. Defina uma classe para um tipo chamado TipoContador. Um objeto desse tipo é utilizado para contar coisas, registrando uma contagem que é um número inteiro não-negativo. Inclua uma função mutator que fixe o contador a uma contagem dada como argumento. Inclua funções-membros para aumentar a contagem em um e diminuir a contagem em um. Assegure-se de que nenhuma função-membro permita que o valor do contador se torne negativo. Inclua também uma função-membro que retorne o valor atual da contagem e um que apresente a contagem como saída. Insira sua definição de classe em um programa-teste. 3. O tipo Point é um tipo de dados bastante simples, mas sob outro nome (a classe modelo pair) esse tipo de dados é definido e utilizado na Standard Template Library (Biblioteca Modelo Padrão) do C++, embora você não precise saber nada sobre a Standard Template Library para fazer este exercício. Escreva uma definição de classe chamada Ponto que pode ser usada para armazenar e manipular a localização de um ponto no plano. Você vai precisar declarar e implementar as seguintes funções-membros: a. uma função-membro set que fixe os dados privados depois que um objeto dessa classe é criado. b. uma função-membro que mova o ponto de uma certa quantidade ao longo das direções vertical e horizontal especificadas pelo primeiro e segundo argumentos. c. uma função-membro para girar o ponto em 90 graus no sentido horário ao redor da origem. d. duas funções inspetoras const para recuperar as coordenadas atuais do ponto.
176
Estruturas e Classes
Documente essas funções com os comentários adequados. Insira sua classe em um programa-teste que peça ao usuário dados para vários pontos, crie os pontos e exercite as funções-membros. 4. Escreva a definição para uma classe chamada BombaDeGasolina para ser usada como modelo para uma bomba em um posto de gasolina. Antes de começar a fazer este exercício, escreva o comportamento que espera de uma bomba de gasolina do ponto de vista do comprador. Abaixo, há uma lista de coisas que se espera que uma bomba de gasolina faça. Se sua lista ficar diferente e você achar que a sua é melhor, consulte o orientador. Você e seu orientador podem decidir juntos qual será o melhor comportamento a implementar. Então implemente e teste seu projeto para a bomba de gasolina. a. Apresentação da quantidade fornecida. b. Apresentação do preço relativo à quantidade fornecida. c. Apresentação do custo por galão, litro ou outra unidade de volume usada no local onde você mora. d. Antes de ser usada, a bomba de gasolina deve zerar a quantidade fornecida e o preço relativo. e. Uma vez em funcionamento, a bomba de gasolina continua a fornecer gasolina, controlar a quantidade fornecida e calcular o preço da quantidade fornecida até parar. f. É necessário algum tipo de controle de parada de fornecimento. Implemente o comportamento da bomba de gasolina como declarações de funções-membros da classe bomba de gasolina, depois escreva implementações dessas funções-membros. Você terá de decidir se há dados sob o controle da bomba de gasolina aos quais o usuário da bomba não deve ter acesso. Se este for o caso, faça com que essas variáveis-membros sejam privadas.
Construtores e Outras Ferramentas Construtores e Outras Ferramentas
Capítulo 7Construtores e Outras Ferramentas Dêem-nos as ferramentas e terminaremos o trabalho. Winston Churchill, transmissão de rádio (9 de fevereiro de 1941)
INTRODUÇÃO Este capítulo apresenta diversas ferramentas importantes para serem utilizadas quando se programa com classes. A mais importante dessas ferramentas são os construtores de classe, um tipo de função utilizada para inicializar objetos da classe. A Seção 7.3 apresenta os vectors como um exemplo de classes e como uma introdução à Standard Template Library (STL). Vectors são semelhantes a vetores, mas podem aumentar e encolher em tamanho. A STL é uma extensa biblioteca de classes predefinidas. A Seção 7.3 pode ser lida agora ou depois. O conteúdo dos Capítulos 8 a 18 não requer o conteúdo da Seção 7.3, portanto, se desejar, você pode adiar a leitura da referida seção. As Seções 7.1 e 7.2 não utilizam o material do Capítulo 5, mas utilizam o do Capítulo 6. A Seção 7.3 requer os Capítulos de 1 a 6, além da Seção 7.1. 7.1
Construtores Um bom início já é metade do caminho. Provérbio
Muitas vezes se quer inicializar alguma ou todas as variáveis-membros de um objeto em sua declaração. Como veremos mais adiante neste livro, existem outras ações de inicialização que você pode querer usar, mas a inicialização de variáveis-membros é a espécie mais comum de inicialização. O C++ inclui recursos especiais para essas inicializações. Quando se define uma classe, pode-se definir um tipo especial de função-membro chamado construtor . Um construtor é uma função-membro que é chamada automaticamente quando um objeto dessa classe é declarado. Utiliza-se o construtor para inicializar os valores de algumas ou de todas as variáveis-membros e para efetuar qualquer outra espécie de inicialização que possa ser necessária. ■
DEFINIÇÕES DE CONSTRUTORES
Define-se um construtor da mesma forma que se define qualquer outra função-membro, a não ser por duas questões: 1. Um construtor deve ter o mesmo nome que a classe. Por exemplo, se a classe se chamar ContaBancaria, qualquer construtor dessa classe deve se chamar ContaBancaria. 2. Uma definição de construtor não pode retornar um valor. Além disso, nenhum tipo, nem mesmo void, pode ser dado no início da declaração da função ou no cabeçalho da função.
178
Construtores e Outras Ferramentas
Por exemplo, suponha que desejemos acrescentar um construtor para inicializar o dia e o mês para objetos de tipo DiaDoAno, o que apresentamos no Painel 6.4, e redefinir as linhas abaixo para incluir um construtor. (Omitimos comentários para economizar espaço, mas eles devem ser incluídos em um programa real.) class DiaDoAno
{ public:
DiaDoAno(int valorDia, int valorMes); //Inicializa o dia e o mês como argumentos.
Construtor
void entrada( ); void saida( ); void set(int novoDia, int novoMes); void set(int novoMes); int getNumeroMes( ); int getDia( ); private: int dia; int mes;
};
Observe que o construtor se chama DiaDoAno, que é o nome da classe. Note também que a declaração (protótipo) do construtor DiaDoAno não começa com void ou qualquer nome de tipo. Finalmente, observe que o construtor é colocado na seção pública da definição de classe. Normalmente, os construtores devem ser funçõesmembros públicas. Se você fizer todos os seus construtores-membros privados, não poderá declarar nenhum objeto daquele tipo classe, o que tornaria a classe completamente inútil. Com a classe redefinida DiaDoAno, dois objetos de tipo DiaDoAno podem ser declarados e inicializados da seguinte forma: DiaDoAno data1(7, 4), data2(5, 5)
Presumindo que a definição do construtor executa a ação de inicializar conforme prometemos, a declaração acima declarará o objeto data1, fixará o valor de data1.dia como 7 e de data1.mes como 9. Assim, o objeto data1 é inicializado de modo a representar a data 7 de setembro. De forma similar, data2 é inicializado de modo a representar a data 5 de maio. O que acontece é que o objeto data1 é declarado e o construtor DiaDoAno é chamado com dois argumentos, 7 e 9. De forma similar, data2 é declarado e o construtor DiaDoAno é chamado com os argumentos 5 e 5. O resultado é conceitualmente equivalente ao seguinte (embora não se possa escrever desta forma em C++): DiaDoAno data1, data2; //PROBLEMAS, MAS CORRIGÍVEIS data1.DiaDoAno(7, 9); //MUITO ILEGAL data2.DiaDoAno(5, 5); //MUITO ILEGAL
Como os comentários indicam, você não pode colocar as três linhas acima em seu programa. É possível tornar a primeira linha aceitável, mas as duas chamadas ao construtor DiaDoAno são ilegais. Um construtor não pode ser chamado da mesma forma que uma função-membro comum. Mesmo assim, o que queremos que aconteça quando escrevemos as três linhas acima é claro, e acontece automaticamente quando se declaram os objetos data1 e data2 da seguinte forma: DiaDoAno data1(7, 9), data2(5, 5);
A definição de um construtor é dada da mesma forma que qualquer outra função-membro. Por exemplo, se você reformular a definição da classe DiaDoAno acrescentando o construtor que acabamos de descrever, também precisa acrescentar uma definição do construtor, que pode ser assim: DiaDoAno::DiaDoAno(int valorDia, int valorMes) { dia = valorDia; mes = valorMes; }
Construtores
179
Como a classe e a função construtora possuem o mesmo nome, o nome DiaDoAno ocorre duas vezes no cabeçalho da função; o DiaDoAno antes do operador de resolução de escopo:: é o nome da classe e o DiaDoAno depois do operador de resolução de escopo é o nome da função construtora. Observe também que não é especificado nenhum tipo para a saída no cabeçalho da definição do construtor, nem mesmo o tipo void. Fora essas questões, um construtor pode ser definido da mesma forma que uma função-membro comum. CONSTRUTOR
Um construtor é uma função-membro de uma classe que possui o mesmo nome que a classe. O construtor é chamado automaticamente quando um objeto da classe é declarado. Construtores são utilizados para inicializar objetos. O construtor deve ter o mesmo nome que a classe de que é membro.
Como acabamos de ilustrar, um construtor pode ser definido exatamente como qualquer outra função-membro. Entretanto, há um modo alternativo e melhor de se definir construtores. A definição prévia do construtor DiaDoAno é totalmente equivalente à versão seguinte: DiaDoAno::DiaDoAno(int valorDia, int valorMes) : dia(valorDia), mes(valorMes) {/*Corpo intencionalmente vazio*/}
O novo elemento mostrado na segunda linha da definição do construtor chama-se seção de inicialização. Como esse exemplo mostra, a seção de inicialização vem depois do parêntese que encerra a lista de parâmetros e antes da chave de abertura do corpo da função. A seção de inicialização consiste em dois-pontos seguidos por uma lista de algumas ou todas as variáveis-membros separadas por vírgulas. Cada variável-membro é seguida por seu valor de inicialização entre parênteses. Observe que os valores de inicialização podem ser fornecidos em termos de parâmetros construtores. O corpo da função em uma definição de construtor com uma seção de inicialização não precisa ser vazio como no exemplo anterior. Por exemplo, a seguinte versão aperfeiçoada da definição de construtor verifica se os argumentos são adequados: DiaDoAno::DiaDoAno(int valorDia, int valorMes) : dia(valorDia), mes(valorMes) { if ((dia < 1) || (dia > 31)) { cout << "Valor de dia ilegal!\n"; exit(1); } if ((mes < 1) || (mes > 12) { cout << "Valor de mês ilegal!\n"; exit(1); } }
Você pode sobrecarregar um nome de construtor como DiaDoAno::DiaDoAno, exatamente como pode sobrecarregar qualquer outro nome de função-membro. Na realidade, em geral os construtores são sobrecarregados para que os objetos possam ser inicializados em mais de uma forma. Por exemplo, no Painel 7.1, redefinimos a classe DiaDoAno de modo que tenha três versões de seu construtor. Essa redefinição sobrecarrega o nome do construtor DiaDoAno de maneira que possa ter dois (como acabamos de explicar), um ou nenhum argumento. Observe que, no Painel 7.1, dois construtores chamam a função-membro testaData para verificar se seus valores de inicialização são adequados. A função-membro testaData é privada, já que foi projetada apenas para ser usada por outras funções-membros e, assim, faz parte dos detalhes da implementação oculta. Omitimos a função-membro set dessa definição de classe reformulada de DiaDoAno. Uma vez que se tenha um bom conjunto de definições de construtor, não há necessidade de quaisquer outras funções-membros para fixar as variáveis-membros da classe. Você pode usar o construtor DiaDoAno do Painel 7.1 com os mesmos objetivos com que usaria a função-membro set (que incluímos na versão antiga da classe exibida no Painel 6.4).
180
Construtores e Outras Ferramentas
Painel 7.1
Classe com construtores ( parte 1 de 2 )
1 2 3
#include #include //para saída using namespace std;
4 5 6 7 8
class DayOfYear
Esta definição de DayOFYear é uma versão aperfeiçoada da classe DayOfYear dada no Painel 6.4.
{ public:
DayOfYear(int monthValue, int dayValue); //Inicializa o dia e o mês com os argumentos.
9 10
DayOfYear(int monthValue); //Inicializa a data para o primeiro dia do mês fornecido.
11 12
DayOfYear( ); //Inicializa a data como 1 de janeiro.
13 14 15 16
void input( ); void output( ); int getMonthNumber( );
construtor-padrão
//Retorna 1 para janeiro, 2 para fevereiro, etc.
17 int getDay( ); 18 private : 19 int month; 20 int day; 21 void testDate( ); 22 };
Isto provoca uma chamada ao construtor-padrão. Observe que não há parênteses.
23 int main( ) 24 { 25 DayOfYear date1(2, 21), date2(5), date3; 26 cout << "Datas inicializadas:\n"; 27 date1.output( ); cout << endl; 28 date2.output( ); cout << endl; 29 date3.output( ); cout << endl; 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
uma chamada explícita ao construtor date1 = DayOfYear(10, 31); cout << "date1 atualizada para a seguinte:\n"; DayOfYear::DayOfYear date1.output( ); cout << endl; return 0;
} DayOfYear::DayOfYear(int monthValue, int dayValue) : month(monthValue), day(dayValue) { testDate( ); } DayOfYear::DayOfYear(int monthValue) : month(monthValue), day(1) { testDate( ); }
45 DayOfYear::DayOfYear( ) : month(1), day(1) 46 {/*Corpo intencionalmente vazio.*/} 47 //utiliza iostream e cstdlib: 48 void DayOfYear::testDate( ) 49 { 50 if ((month < 1) || (month > 12)) 51 { 52 cout << "Valor ilegal para o mês!\n"; 53 exit(1);
Construtores Painel 7.1 54 55 56 57 58 59 60 }
181
Classe com construtores ( parte 2 de 2 )
} if ((day
< 1) || (day > 31))
{
cout << "valor ilegal para o dia!\n"; exit(1); }
DIÁLOGO PROGRAMA-USUÁRIO Datas 21 de 1o de 1o de data1 31 de
inicializadas: fevereiro maio janeiro atualizada para: outubro
CONSTRUTORES SEM ARGUMENTOS É importante não utilizar parênteses quando se declara uma variável classe e se deseja que o construtor seja invocado sem argumentos. Por exemplo, considere a linha seguinte do Painel 7.1: DiaDoAno data1(21, 2), data2(5), data3; O objeto data1 é inicializado pelo construtor
que requer dois argumentos, o objeto data2 é inicializado pelo construtor que requer um argumento e o objeto data3 é inicializado pelo construtor que não requer argumentos. É tentador pensar que se devem utilizar parênteses vazios quando se declara uma variável para a qual se deseja o construtor sem nenhum argumento invocado, mas há um motivo para isso não ser feito. Considere a seguinte linha, que aparentemente deveria declarar a variável data3 e invocar o construtor sem argumentos: DiaDoAno data3( ); //PROBLEMA! Não é o que você pensa que é.
O problema é que, embora você pretenda que isso seja uma declaração e uma invocação ao construtor, o compilador vê isso como uma declaração (protótipo) de uma função chamada data3 que não possui parâmetros e que retorna um valor de tipo DiaDoAno. Como uma função chamada data3 que não possua parâmetros e que retorne um valor de tipo DiaDoAno é perfeitamente legal, esta notação sempre tem esse significado. Utiliza-se uma notação diferente (sem parênteses) quando se quer invocar um construtor sem argumentos.
CHAMANDO UM CONSTRUTOR Um construtor é chamado automaticamente quando um objeto é declarado, mas você deve dar o argumento para o construtor quando declara o objeto. Um construtor também pode ser chamado explicitamente, mas a sintaxe é diferente da usada para funções-membros comuns.
SINTAXE
PARA UMA DECLARAÇÃO DE OBJETO QUANDO SE Nome_Classe Nome_Variavel(Argumentos_Para_Construtor);
TEM CONSTRUTORES
EXEMPLO DiaDoAno feriado(7, 9);
SINTAXE PARA UMA CHAMADA EXPLÍCITA AO CONSTRUTOR Variavel = Nome_Construtor(Argumentos_Para_Construtor);
EXEMPLO feriado = DiaDoAno(25, 12);
Um construtor deve ter o mesmo nome que a classe da qual é membro. Assim, nas descrições de sintaxe acima, Nome_Classe e Nome_Construtor são o mesmo identificador.
CHAMADAS EXPLÍCITAS A CONSTRUTORES Um construtor é chamado explicitamente sempre que se declara um objeto do tipo classe, mas também pode ser chamado novamente depois que o objeto tenha sido declarado. Isso permite que se fixem de maneira conve■
182
Construtores e Outras Ferramentas
niente todos os membros de um objeto. Os detalhes técnicos são os seguintes: chamar o construtor cria um objeto anônimo com novos valores. Um objeto anônimo é um objeto que não foi nomeado (ainda) por nenhuma variável. O objeto anônimo pode ser atribuído ao objeto nomeado. Por exemplo, aqui está uma chamada ao construtor DiaDoAno que cria um objeto anônimo para a data 25 de dezembro. Esse objeto anônimo é atribuído à variável feriado (que foi declarada como sendo do tipo DiaDoAno) de modo que feriado também represente a data 25 de dezembro. 1 feriado = DiaDoAno(25, 12);
(Como você pode concluir pela notação, um construtor às vezes se comporta como uma função que retorna um objeto do seu tipo classe.) Observe que, quando se invoca explicitamente um construtor sem argumentos, os parênteses devem ser incluídos, da seguinte forma: feriado = DiaDoAno( );
Os parênteses são omitidos somente quando se declara uma variável do tipo classe e se quer invocar um construtor sem argumentos como parte da declaração. SEMPRE INCLUA UM CONSTRUTOR-PADRÃO Um construtor que não requeira argumentos é chamado de construtor-padrão. Esse nome pode provocar confusões, porque às vezes ele é gerado automaticamente e outras não. Vamos lhe contar a história toda. Se você definir uma classe e não incluir nenhum construtor de nenhum tipo, um construtor-padrão será automaticamente criado. Esse construtor-padrão não faz nada, mas fornece a você um objeto não-inicializado do tipo classe, que pode ser atribuído a uma variável do tipo classe. Se sua definição de classe incluir um ou mais construtores de qualquer tipo, nenhum construtor é gerado automaticamente. Assim, por exemplo, suponha que você defina uma classe chamada ClasseAmostra. Se você incluir um ou mais construtores que requeiram, cada um, um ou mais argumentos, mas não incluir um construtor-padrão em sua definição de classe, não há construtor-padrão e qualquer declaração como a seguinte será ilegal: ClasseAmostra umaVariavel;
O problema com a declaração acima é que ela pede ao compilador para invocar o construtor-padrão, mas nesse caso não existe construtor-padrão. Para tornar isso concreto, suponha que você defina uma classe assim: class ClasseAmostra
{ public:
ClasseAmostra(int parametro1, double parametro2); private: int dado1; double dado1;
};
Você deve reconhecer a seguinte linha como uma forma legal de declarar um objeto de tipo ClasseAmostra e chamar o construtor para essa classe: ClasseAmostra minhaVariavel(7, 7.77);
Entretanto, a linha seguinte é ilegal: ClasseAmostra suaVariavel;
O compilador interpreta a declaração acima como a inclusão de uma chamada a um construtor sem argumentos, mas não há definição para um construtor com zero argumento. Você precisa acrescentar dois argumentos à declaração de suaVariavel ou acrescentar uma definição de construtor para um construtor sem argumentos. Se você redefinir a classe ClasseAmostra da seguinte forma, a declaração acima de suaVariavel será legal: class ClasseAmostra
{ public:
ClasseAmostra(int parametro1, double parametro2); ClasseAmostra( ); Construtor-padrão
1.
Observe que este processo é mais complicado do que simplesmente alterar os valores das variáveis-membros. Por razões de eficiência, portanto, você pode querer continuar usando as funções-membros chamadas set no local de uma chamada explícita a um construtor.
Construtores
183
void fazerAlgo( ); private: int dado1; double dado1; }; Para evitar esse tipo de confusão, inclua sempre um construtor-padrão em toda a classe que definir. Se não quiser que o construtor-padrão inicialize quaisquer variáveis-membros, simplesmente lhe dê um corpo vazio quando o implementar. A seguinte definição de construtor é perfeitamente legal. Não faz nada, mas cria um objeto não-inicializado: ClasseAmostra::ClasseAmostra( ) { /* Não faz nada.*/}
CONSTRUTORES SEM ARGUMENTOS
Um construtor que não requer argumentos é chamado de construtor-padrão. Quando se declara um objeto e se deseja que o construtor com zero argumento seja chamado, não se incluem parênteses. Por exemplo, para declarar um objeto e passar dois argumentos para o construtor, pode-se fazer o seguinte: DiaDoAno data1(31, 12); Entretanto, se você quiser que o construtor com zero argumento seja usado, declare o objeto assim: DiaDoAno data2; Não declare o objeto assim: DiaDoAno data2( ); //PROBLEMA! (O problema é que essa sintaxe declara uma função que retorna um objeto DiaDoAno e que não possui parâmetros.) Você deve, contudo, incluir os parênteses quando invocar explicitamente um construtor sem argumentos, como mostrado abaixo: data1 = DiaDoAno( );
1. Suponha que seu programa contenha a seguinte definição de classe (com definições das funções-membros): class SuaClasse { public:
SuaClasse(int novaInfo, char maisNovaInfo); SuaClasse( ); void fazerAlgo( ); private: int informacao; char maisInformacao;
}; Quais das seguintes linhas é ilegal? SuaClasse umObjeto(42, ’A’); SuaClasse outroObjeto; SuaClasse maisOutroObjeto( ); umObjeto = SuaClasse(99, ’B’); umObjeto = SuaClasse( ); umObjeto = SuaClasse; 2. O que é um construtor-padrão? Toda classe tem um construtor-padrão?
CLASSE ContaBancaria
O Painel 7.2 contém a definição de uma classe que representa uma conta bancária simples inserida em um pequeno programa de demonstração. Uma conta bancária, dessa forma, possui dois conjuntos de dados: o
184
Construtores e Outras Ferramentas
saldo bancário e a taxa de juros. Observe que representamos a conta bancária como dois valores de tipo int, um para os reais e outro para os centavos. Isso ilustra o fato de que a representação interna dos dados não precisa ser simplesmente uma variável-membro para cada conjunto conceitual de dados. Talvez pareça que o saldo devesse ser representado como um valor de tipo double, em vez de dois valores int. Entretanto, uma conta abriga um número exato de reais e centavos, e um valor de tipo double é, falando na prática, uma quantidade aproximada. Além disso, um saldo como R$ 323,52 não é um sinal de real diante de um valor em ponto flutuante. R$ 323,52 não pode ter mais ou menos de dois dígitos depois do ponto decimal. Não se pode ter um saldo de R$ 323,523, e uma variável-membro do tipo double permitiria que houvesse. Não é impossível ter uma conta com frações de centavos, mas não é o que desejamos para uma conta bancária. Observe que o programador que utiliza a classe ContaBancaria pode pensar no saldo como um valor de tipo double ou como dois valores de tipo int (para reais e centavos). As funções acessor e mutator permitem que o programador leia e fixe o saldo como sendo um double ou dois ints. O programador que está usando a classe não precisa e não deve pensar em quaisquer variáveis-membros subjacentes. Isso é parte da implementação "oculta" do programador que utiliza a classe. Note que a função mutator setSaldo bem como os nomes dos construtores estão sobrecarregados. Observe também que todos os construtores e funções mutator verificam os valores para garantir que sejam apropriados. Por exemplo, uma taxa de juros não pode ser negativa. Um saldo pode ser negativo, mas não se pode ter um número positivo de reais e um número negativo de centavos. Essa classe possui funções-membros privadas: dolaresParte, centavosParte, redondo e fracao. Essas funções-membros são tornadas privadas porque são projetadas somente para ser usadas nas definições de outras funções-membros.
Painel 7.2
Classe ContaBancaria (parte 1 de 5)
1 2 3 4
#include #include #include using namespace std;
5 6 7 8 9 10 11
//Os dados consistem em dois itens: uma quantia de dinheiro para o saldo //e uma porcentagem para a taxa de juros. class BankAccount { public: BankAccount(double balance, double rate); //Inicializa saldo e taxa de juros de acordo com os argumentos.
12 13 14
BankAccount(int dollars, int cents, double rate); //Inicializa o saldo como $dollars.cents. Para um saldo negativo, tanto //os dólares quanto os centavos devem ser negativos. Inicializa a taxa de juros como uma porcentagem.
15 16 17
BankAccount(int dollars, double rate); //Inicializa o saldo como $dollars.00 e //inicializa a taxa de juros como uma porcentagem.
18 19
BankAccount( ); //Inicializa o saldo como $0.00 e a taxa de juros como 0.0%.
20 21 22 23 24 25 26 27
void update( );
28 29 30
//Pós-condição: Um ano de juros simples foi acrescentado ao saldo. void input( ); void output( ); double getBalance( ); int getDollars( ); int getCents( ); double getRate( );//Retorna a taxa de juros como uma porcentagem. void setBalance(double balance); void setBalance(int dollars, int cents);
//Verifica se os argumentos são ambos não-negativos ou não-positivos.
Construtores
Painel 7.2
31 32 33 34 35 36 37 38 39 40 41 42
Classe ContaBancaria (parte 2 de 5)
void setRate(double newRate);
//Se newRate é não-negativa, torna-se a nova taxa. Caso contrário, aborta o programa. Membros privados private: //Uma quantia negativa é representada como dólares e centavos negativos. //Por examplo, $4.50 negativo fixa accountDollars como -4 e accountCents como -50. int accountDollars; //de saldo int accountCents; //de saldo double rate;//como uma porcentagem int dollarsPart(double amount); int centsPart(double amount); int round(double number);
43 double fraction(double percent); 44 //Converte uma porcentagem em fração. Por exemplo, fraction(50.3) retorna 0.503. 45 }; Esta declaração provoca uma chamada ao 46 int main( ) construtor-padrão. Observe que não há parênteses. 47 { 48 BankAccount account1(1345.52, 2.3),account2; 49 cout << "conta1 inicializada da seguinte forma:\n"; 50 account1.output( ); 51 cout << "conta2 inicializada da seguinte forma:\n"; 52 account2.output( ); 53 54 55
account1 = BankAccount(999, 99, 5.5); cout << "conta atualizada:\n"; account1.output( );
56 57 58 59
cout << "Forneça novos dados para a conta2:\n"; account2.input( ); cout << "conta atualizada:\n"; account2.output( );
60 61 62
account2.update( ); cout << "Em um ano a conta2 apresentará:\n"; account2.output( );
63 64 }
return 0;
uma chamada explícita ao construtor BankAccount::BankAccount
65 BankAccount::BankAccount(double balance, double rate) 66 : accountDollars(dollarsPart(balance)), accountCents(centsPart(balance)) 67 { 68 setRate(rate); 69 } 70 BankAccount::BankAccount(int dollars, int cents, double rate) 71 { Essas funções verificam se os 72 setBalance(dollars, cents); dados são adequados. 73 setRate(rate); 74 } 75 BankAccount::BankAccount(int dollars, double rate) 76 : accountDollars(dollars), accountCents(0) 77 { 78 setRate(rate);
185
186
Construtores e Outras Ferramentas
Painel 7.2
Classe
ContaBancaria
(parte 3 de 5)
79
}
80 81
BankAccount::BankAccount( ): accountDollars(0), accountCents(0), rate(0.0) {/*Corpo propositadamente vazio.*/}
82 83 84 85 86 87 88
void BankAccount::update( ) { double balance = accountDollars + accountCents*0.01; balance = balance + fraction(rate)*balance; accountDollars = dollarsPart(balance); accountCents = centsPart(balance); }
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113
//Utiliza iostream: Para uma melhor definição de void BankAccount::input( ) o Exercício de Autoteste 3. { double balanceAsDouble; cout << "Forneça o saldo bancário $"; cin >> balanceAsDouble; accountDollars = dollarsPart(balanceAsDouble); accountCents = centsPart(balanceAsDouble); cout << "Forneça a taxa de juros (SEM o símbolo de porcentagem): "; cin >> rate; setRate(rate); } //Utiliza iostream e cstdlib: void BankAccount::output( ) { int absDollars = abs(accountDollars); int absCents = abs(accountCents); cout << "Saldo bancário: $"; if (accountDollars < 0) cout << "-"; cout << absDollars; if (absCents >= 10) cout << "." << absCents << endl;
114 115 }
BankAccount::input veja
else
cout << "." << ’0’ << absCents << endl; cout << "Taxa: " << rate << "%\n";
116 double BankAccount::getBalance( ) 117 { 118 return (accountDollars + accountCents*0.01); 119 } 120 int BankAccount::getDollars( ) 121 { 122 return accountDollars; 123 } 124 int BankAccount::getCents( ) 125 { 126 return accountCents; 127 } 128
double BankAccount::getRate(
)
O programador que utiliza a classe não se importa se o saldo é armazenado com um real ou dois ints.
Construtores
Painel 7.2
129 { 130 131 }
Classe ContaBancaria (parte 4 de 5) return rate;
132 void BankAccount::setBalance(double balance) 133 { 134 accountDollars = dollarsPart(balance); 135 accountCents = centsPart(balance); 136 } 137 //Utiliza cstdlib: 138 void BankAccount::setBalance(int dollars, int cents) 139 { if ((dollars < 0 && cents > 0) || (dollars > 0 && cents < 0)) 140 141 { 142 cout << "Dados inconsistentes.\n"; 143 exit(1); 144 } 145 accountDollars = dollars; 146 accountCents = cents; 147 } 148 //Utiliza cstdlib: 149 void BankAccount::setRate(double newRate) 150 { if (newRate >= 0.0) 151 152 rate = newRate; else 153 154 { 155 cout << "Não pode haver uma taxa de juros negativa.\n"; 156 exit(1); 157 } 158 } 159 160 161 162 163 164 165 166 167 168 169 170 171
int BankAccount::dollarsPart(double amount) Esta poderia ser uma função regular em vez { de função-membro, mas como funçãoreturn static_cast(amount); membro podemos torná-la privada. } //Utiliza cmath: int BankAccount::centsPart(double amount) { double doubleCents = amount*100; int intCents = (round(fabs(doubleCents)))%100;//% pode se comportar mal com negativos if (amount < 0) Estas funções poderiam ser regulares em vez de funções-membros, intCents = -intCents; mas como funções-membros podemos torná-las privadas. return intCents; }
172 //Utiliza cmath: 173 int BankAccount::round(double number) 174 { return static_cast(floor(number + 0.5)); 175 176 } 177 double BankAccount::fraction(double percent) 178 { return (percent/100.0); 179 180 }
Se isso não parecer claro, veja a explicação sobre round no Capítulo 3, Seção 3.2.
187
188
Construtores e Outras Ferramentas
Painel 7.2
Classe ContaBancaria (parte 5 de 5)
DIÁLOGO PROGRAMA-USUÁRIO conta1 inicializada da seguinte forma: Saldo bancário: R$ 1.345,52 Taxa de juros: 2,3% conta2 inicializada da seguinte forma: Saldo bancário: R$ 0,00 Taxa de juros: 0% conta1 atualizada: Saldo bancário: R$ 999,99 Taxa de juros: 5,5% Forneça novos dados para a conta2: Forneça o saldo bancário: R$ 100,00 Forneça a taxa de juros (SEM o símbolo de porcentagem): 10 conta2 atualizada: Saldo bancário: R$ 100 Taxa de juros: 10% Em um ano a conta2 apresentará: Saldo bancário: R$ 110 Taxa de juros: 10%
3. A função ContaBancaria::entrada no Painel 7.2 lê o saldo da conta com um valor de tipo double. Quando o valor é armazenado na memória do computador na forma binária, isso pode criar um pequeno erro. Este normalmente não seria notado, e a função é boa o bastante para a classe de demonstração ContaBancaria. Gastar tempo demais em análise numérica prejudicaria a mensagem em questão. Mesmo assim, a função entrada não é suficientemente boa para as operações bancárias. Reescreva a função ContaBancaria::entrada para que leia uma quantia como R$ 78,96 como o int 76, e três valores char ’.’, ’9’ e ’6’. Você pode presumir que o usuário sempre forneça dois dígitos para os centavos, como 99,00 em vez de apenas 99 e nada mais. Dica: a fórmula seguinte converterá um dígito no valor int correspondente, como ’6’ em 6. static_cast (digito) - static_cast(’0’)
■
VARIÁVEIS-MEMBROS DE TIPO CLASSE
Uma classe pode ter uma variável-membro cujo tipo é de outra classe. Em geral não é preciso fazer nada de especial para se ter uma variável-membro de classe, mas há uma notação especial para permitir a invocação do construtor da variável-membro dentro do construtor da classe exterior. Fornecemos um exemplo no Painel 7.3. A classe Feriado no Painel 7.3 poderia ser utilizada por algum departamento de trânsito para ajudar a controlar que feriados necessitariam de reforço na verificação dos estacionamentos (envolvendo parquímetros e bilhetes de zona azul). É uma classe bastante simplificada. Uma classe de verdade teria mais funções-membros, mas a classe Feriado é suficiente para ilustrar a questão. A classe Feriado possui duas variáveis-membros. A variável-membro reforcoEstacionamento é uma variávelmembro comum do tipo simples bool. A variável-membro data é do tipo classe DiaDoAno. Reproduzimos a seguir uma definição de construtor do Painel 7.3: Feriado::Feriado(int dia, int mes, bool oReforco) : data(dia, mes), reforcoEstacionamento(oReforco) {/* Propositadamente vazio*/}
Observe que fixamos a variável-membro reforcoEstacionamento na seção de inicialização da forma usual, ou seja, com reforcoEstacionamento(oReforco)
Construtores
189
A variável-membro data é um membro do tipo classe DiaDoAno. Para inicializar data, precisamos invocar um construtor da classe DiaDoAno (o tipo de data). Isso é feito na seção de inicialização com a notação similar data(dia, mes) Painel 7.3
1 2 3
Variável-membro classe ( parte 1 de 2)
#include #include using namespace std;
4 class DayOfYear 5 { 6 public: 7 DayOfYear(int monthValue, int dayValue); 8 DayOfYear(int monthValue); 9 DayOfYear( ); 10 void input( ); 11 void output( ); 12 int getMonthNumber( ); 13 int getDay( ); 14 private: 15 int month; 16 int day; 17 void testDate( ); 18 };
A classe DayOfYear é a mesma do Painel 7.1, mas repetimos todos os detalhes necessários para que você possa compreender esta discussão.
19 class Holiday 20 { 21 public: 22 Holiday( );//Inicializa como 1 de janeiro sem reforço na verificação dos estacionamentos 23 Holiday(int month, int day, bool theEnforcement); 24 void output( ); 25 private: variável-membro de um tipo classe 26 DayOfYear date; 27 bool parkingEnforcement;//verdadeiro se houver reforço 28 }; 29 int main( ) 30 { 31 Holiday h(2, 14, true); 32 cout << "Testando a classe Feriado.\n"; 33 h.output( ); Invocações de construtores 34 return 0; da classe DayOfYear. 35 } 36 37 Holiday::Holiday( ) : date(1, 1), . parkingEnforcement(false) 38 {/*Propositadamente vazio*/} 39 Holiday::Holiday(int month, int day, bool theEnforcement) 40 : date(month, day), parkingEnforcement(theEnforcement) 41 {/*Propositadamente vazio*/} 42 void Holiday::output( ) 43 { 44 date.output( ); 45 cout << endl; 46 if (parkingEnforcement) 47 cout << "As leis de estacionamento serão reforçadas.\n"; 48 else 49 cout << "As leis de estacionamento serão reforçadas.\n";
190
Construtores e Outras Ferramentas
Painel 7.3
Variável-membro classe ( parte 2 de 2)
50 } 51 DayOfYear::DayOfYear(int monthValue, int dayValue) 52 : month(monthValue), day(dayValue) 53 { 54 testDate( ); 55 } 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
//utiliza iostream e cstdlib: void DayOfYear::testDate( ) { if ((month < 1) || (month > 12)) { cout << "Valor ilegal para o mês!\n"; exit(1); } if ((day < 1) || (day > 31)) { cout << "Valor ilegal para o dia!\n"; exit(1); } } //Utiliza iostream: void DayOfYear::output( ) { switch (month) { case 1: cout << "Janeiro "; break; case 2: cout << "Fevereiro "; break; case 3: cout << "Março "; break;
As linhas omitidas estão no Painel 6.3, mas não suficientemente óbvias para que você não precise ir até lá.
. . .
82 83 84 85 86 87 88
case 11:
cout << "Novembro "; break; case 12:
cout << "Dezembro "; break; default:
cout << "Erro em DayOfYear::output. Entre em contato com o fornecedor do software."; }
89 90 }
cout << day;
DIÁLOGO PROGRAMA-USUÁRIO Testando a classe Feriado. 15 de novembro As leis de estacionamento serão reforçadas.
A notação data(dia, mes) é uma invocação do construtor da classe DiaDoAno com os argumentos dia e mes para inicializar as variáveis-membros de data. Observe que essa notação é análoga ao modo como se declara a va-
Mais Ferramentas
191
riável data de tipo DiaDoAno. Observe também que os parâmetros do construtor da classe maior Feriado podem ser usados na invocação do construtor da variável-membro. 7.2
Mais Ferramentas Inteligência... é a capacidade de criar objetos artificiais, especialmente ferramentas para construir ferramentas. Henri Bergson, A Evolução Criadora
Esta seção trata de três tópicos que, embora importantes, não se encaixavam bem antes desse ponto. Os três tópicos são parâmetros const para classes, funções inline e membros estáticos de classe.
MODIFICADOR DE PARÂMETROS const Um parâmetro chamado por referência é mais eficiente que um parâmetro chamado por valor. Um parâmetro chamado por valor é uma variável local inicializada com o valor de seu argumento, de modo que, quando a função é chamada, há duas cópias do argumento. Com um parâmetro chamado por referência, o parâmetro é apenas um "guardador" de lugar que é substituído pelo argumento, então há apenas uma cópia do argumento. Para parâmetros de tipos simples, como int ou double, a diferença na eficiência é negligenciável, mas para parâmetros de classe a diferença pode às vezes ser importante. Assim, pode ser interessante utilizar um parâmetro chamado por referência em vez de um parâmetro chamado por valor para uma classe, mesmo que a função não altere o parâmetro. Se você está usando um parâmetro chamado por referência e sua função não altera o valor do parâmetro, você pode marcar o parâmetro para que o compilador saiba que o parâmetro não deve ser alterado. Para fazer isso, acrescente o modificador const diante do tipo do parâmetro. O parâmetro é, então, chamado de parâmetro constante ou parâmetro constante chamado por referência . Por exemplo, no Painel 7.2, definimos uma classe chamada ContaBancaria para contas bancárias simples. Em algum programa você pode querer escrever uma função de valor booleano para testar qual das duas contas tem maior saldo. A definição da função pode ser assim: ■
bool maior(ContaBancaria
conta1, ContaBancaria conta2) //Retorna true se o saldo da conta1 for maior que //o da conta2. Caso contrário, retorna false. { return(conta1.getSaldo( ) > conta2.getSaldo( )); }
Isso é perfeitamente legal. Os dois parâmetros são chamados por valor. Entretanto, seria mais eficiente e mais comum transformar os parâmetros em parâmetros constantes chamados por referência, desta forma: bool maior(constContaBancaria&
conta1,
const ContaBancaria&
conta2) //Retorna true se o saldo da conta1 for maior que //o da conta2. Caso contrário, retorna false. { return(conta1.getSaldo( ) > conta2.getSaldo( )); }
Observe que a única diferença é que transformamos o parâmetro em chamado por referência acrescentando & e adicionamos modificadores const. Se houver uma declaração de função, a mesma mudança precisa ser feita nos parâmetros da declaração de função. Parâmetros constantes são uma forma automática de verificação de erros. Se sua definição de função contiver um erro que cause uma alteração imprevista do parâmetro constante, o compilador emitirá uma mensagem de erro. O modificador de parâmetros const pode ser usado com qualquer tipo de parâmetro; entretanto, normalmente é usado apenas com parâmetros chamados por referência para classes (e com certos parâmetros cujos argumentos correspondentes são extensos, como vetores). Suponha que você invoque uma função-membro para um objeto de uma classe, como a classe ContaBancaria do Painel 7.2. Por exemplo:
192
Construtores e Outras Ferramentas
ContaBancaria minhaConta; minhaConta.entrada( ); minhaConta.saida( );
A invocação da função-membro entrada muda os valores das variáveis-membros no objeto que faz a chamada minhaConta. Assim, o objeto que faz a chamada se comporta como uma espécie de parâmetro chamado por referência; a invocação da função pode alterar o objeto que faz a chamada. Às vezes não se quer alterar as variáveismembros do objeto que faz a chamada. Por exemplo, a função-membro saida não deve alterar os valores das variáveis-membros do objeto que faz a chamada. Pode-se utilizar o modificador const para dizer ao compilador que uma invocação de função-membro não deve alterar o objeto que faz a chamada. O modificador const se aplica a objetos que fazem a chamada do mesmo modo que aos parâmetros. Caso se tenha uma função-membro que não deva alterar o valor de um objeto que faz a chamada, pode-se marcar a função com o modificador const; então, o compilador emitirá uma mensagem de erro se o código de sua função alterar inadvertidamente o valor de um objeto que faz a chamada. No caso de uma função-membro, o const vai até o fim da declaração de função, até antes do ponto-e-vírgula final, como mostrado a seguir: class ContaBancaria
{ public:
... void saida( ) const; ...
O modificador const deve ser usado tanto na declaração quanto na definição de função. Portanto, a definição de função para saida começará assim: void ContaBancaria::saida( ) const
{ ...
O restante da definição de função será o mesmo do Painel 7.2. USO INCONSISTENTE DE const
O uso do modificador const é uma questão de tudo ou nada. Se você usar const para um parâmetro de um tipo particular, deve usá-lo para todos os outros parâmetros que tiverem esse tipo e que não sejam alterados pela chamada de função. Além disso, se o tipo for um tipo classe, você deve usar também o modificador const para toda função-membro que não alterar o valor do objeto que faz a chamada. A razão disso tem a ver com chamadas de função dentro de chamadas de função. Por exemplo, considere a seguinte definição da função bemvindo: void bemvindo(const ContaBancaria& suaConta)
{ cout << "Bem-vindo ao nosso banco.\n" << "O status da sua conta é:\n"; suaConta.saida( ); }
Se você não acrescentar o modificador const à declaração de função para a função-membro saida, a função bemvindo produzirá uma mensagem de erro. A função membro bemvindo não altera o objeto que faz a chamada preco. Entretanto, quando o compilador processa a definição de função para bemvindo, pensará que bemvindo altera (ou pelo menos deveria alterar) o valor de suaConta. Isso acontece porque, quando está traduzindo a definição de função para bemvindo, tudo o que o compilador sabe a respeito da função-membro saida é a declaração de função para saida. Se a declaração de função não contiver um const que diga ao compilador que o objeto que fez a chamada não será alterado, o compilador presume que o objeto que fez a chamada será alterado. Assim, se você utilizar o modificador const com parâmetros do tipo ContaBancaria, deve utilizar const também com todas as funções-membros ContaBancaria que não alterarem os valores de seus objetos que fazem as chamadas. Em particular, a declaração de função para a função-membro saida deve incluir um const. No Painel 7.4, reescrevemos a definição da classe ContaBancaria dada no Painel 7.2, mas dessa vez utilizamos o modificador const nos lugares adequados. No Painel 7.4, acrescentamos também duas funções maior e bemvindo, das quais já tratamos, e que possuem parâmetros constantes.
Mais Ferramentas
193
MODIFICADOR DE PARÂMETROS const Se você colocar o modificador const antes do tipo para um parâmetro chamado por referência, o parâmetro é chamado de parâmetro constante. Quando você acrescenta o const está dizendo ao compilador que aquele parâmetro não deve ser alterado. Se você cometer um erro em sua definição da função de modo que esta altere o parâmetro constante, o compilador dará uma mensagem de erro. Parâmetros de um tipo classe que não são alterados pela função normalmente deveriam ser parâmetros constantes chamados por referência em vez de parâmetros chamados por valor. Se uma função-membro não alterar o valor do objeto que faz a chamada, você pode marcar a função acrescentando o modificador const à declaração de função. Se você cometer um erro em sua definição de função, de modo que esta altere o objeto que faz a chamada e a função esteja marcada com um const, o compilador emitirá uma mensagem de erro. const é colocado ao final da declaração de função, antes do ponto-e-vírgula final. O cabeçalho da definição de função também deve ter um const para ficar igual à declaração da função.
EXEMPLO class Amostra
{ public:
Amostra( ); void entrada( ); void saida( ) const; private: int coisa; double outraCoisa; }; int compare(const Amostra& s1, const Amostra& s2);
O uso do modificador const é uma questão de tudo ou nada. Você deve usar o modificador const sempre que for apropriado para um parâmetro de classe e sempre que for apropriado para uma função-membro da classe. Se você não usar const todas as vezes que for apropriado para uma classe, nunca deve usá-lo para essa classe.
4. Por que seria incorreto acrescentar o modificador const, como mostrado a seguir, à declaração para a função-membro entrada da classe ContaBancaria dada no Painel 7.2? class ContaBancaria
{ public: void saida( ) const;
...
5. Quais são as diferenças e semelhanças entre um parâmetro chamado por valor e um parâmetro constante chamado por referência? Seguem-se declarações que ilustram a questão: void chamadoPorValor(int x); void chamadoPorReferenciaConstante(const int& x);
6. Dadas as definições const int x = 17 class A
{ public:
A( ); A(int n); int f( )const; int g(const A& x); private: int i;
};
Cada uma dessas três palavras-chave const é uma promessa ao compilador, e o compilador vai executá-la. Qual é a promessa em cada caso?
194
Construtores e Outras Ferramentas
Painel 7.4
Modificador de parâmetros const (parte 1 de 2)
1 2 3 4
#include #include #include using namespace std;
5 6 7 8 9 10 11
//Os dados consistem em dois itens: uma quantia de dinheiro para o saldo //e uma porcentagem para a taxa de juros. class BankAccount { public: BankAccount(double balance, double rate); //Inicializa saldo e taxa de juros de acordo com os argumentos.
Esta é a classe do Painel 7.2. uitilizando o modificador const.
12 13 14
BankAccount(int dollars, int cents, double rate); //Inicializa o saldo como $dollars.cents. Para um saldo negativo, tanto //os dólares quanto os centavos devem ser negativos. Inicializa a taxa de juros como uma porcentagem.
15 16 17
BankAccount(int dollars, double rate); //Inicializa o saldo como $dollars.00 e //inicializa a taxa de juros como uma porcentagem.
18 19
BankAccount( ); //Inicializa o saldo como $0.00 e a taxa de juros como 0.0%.
20 21 22 23 24 25 26 27
void update( );
28 29 30
//Pós-condição: Um ano de juros simples foram acrescentados ao saldo. void input( ); void output( ) const; double getBalance( ) const; int getDollars( ) const; int getCents( ) const; double getRate( ) const;//Retorna a taxa de juros como uma porcentagem. void setBalance(double balance); void setBalance(int dollars, int cents);
//Verifica se os argumentos são ambos não-negativos ou não-positivos.
31 void setRate(double newRate); 32 //Se newRate é não-negativo, torna-se a nova taxa. Caso contrário, aborta o programa. 33 34 private: 35 //Uma quantia negativa é representada como dólares e centavos negativos. 36 //Por exemplo, $4.50 negativo fixa accountDollars como -4 e accountCents como -50. 37 int accountDollars; //de saldo 38 int accountCents; //de saldo 39 double rate;//como uma porcentagem 40 int dollarsPart(double amount) const; 41 int centsPart(double amount) const; int round(double number) const; 42 43 double fraction(double percent) const; 44 //Converte uma porcentagem em fração. Por exemplo, fraction(50.3) retorna 0.503. 45 }; 46 //Retorna true se o saldo na account1 for maior que 47 //o da account2. Caso contrário, retorna false. 48 bool isLarger(const BankAccount& account1, const BankAccount& account2);
Mais Ferramentas
Painel 7.4
195
Modificador de parâmetros const (parte 2 de 2)
49 void welcome(const BankAccount& yourAccount); 50 int main( ) 51 { 52 BankAccount account1(6543.21, 4.5), account2; 53 welcome(account1); 54 cout << "Informe os dados da conta 2:\n"; 55 account2.input( ); if (isLarger(account1, account2)) 56 57 cout << "conta1 é maior.\n"; else 58 59 cout << "account2 is at least as large as account1.\n"; return 0; 60 61 } 62 63 bool isLarger(const BankAccount& account1, const BankAccount& account2) 64 { return(account1.getBalance( ) > account2.getBalance( )); 65 66 }
67 void welcome(const BankAccount& yourAccount) 68 { 69 cout << "Bem-vindo ao nosso banco.\n" 70 << "O status da sua conta é:\n"; 71 yourAccount.output( ); 72 } 73 //Utiliza iostream e cstdlib: 74 void BankAccount::output( ) const 75
76 77
DIÁLOGO PROGRAMA-USUÁRIO Bem-vindo ao nosso banco. O status da sua conta é: Saldo bancário: R$ 6.543,21 Taxa de juros: 4,5% Informe os dados da conta 2: Informe o saldo da conta: R$ 100,00 Informe a taxa de juros (sem sinal de porcentagem): 10 conta1 é maior.
■
FUNÇÕES INLINE
Pode-se dar a definição completa de uma função-membro dentro da definição de sua classe. Tais definições são chamadas de definições de funções inline. Essas definições inline geralmente são usadas em definições de função bem curtas. O Painel 7.5 mostra a classe do Painel 7.4 reescrita com diversas funções inline. As funções inline são mais do que uma variante de notação do tipo de definições de funções-membros que já vimos. O compilador trata uma função inline de forma especial. O código para uma declaração de função inline é inserido em cada posição em que a função é invocada. Isso economiza o gasto adicional de uma invocação de função.
196
Construtores e Outras Ferramentas
Se tudo o mais for igual, uma função inline deve ser mais eficiente que uma função definida da forma usual e, portanto, preferível a esta. Entretanto, dificilmente (ou talvez nunca) tudo o mais é igual. As funções inline têm a desvantagem de misturar a interface e a implementação de uma classe e, assim, infringir o princípio da encapsulação. Temos, portanto, vantagens e desvantagens. Geralmente se acredita que apenas definições de função muito curtas devem ser feitas inline. Para definições de função longas, a versão inline pode ser menos eficiente, porque grandes trechos de código são repetidos freqüentemente. Fora esta regra geral, você terá de decidir por si próprio se utiliza ou não funções inline. Qualquer função pode ser definida como inline. Para definir uma função não-membro como inline, é só colocar a palavra-chave inline antes da declaração e da definição de função. Não utilizaremos nem discutiremos mais funções não-membros inline neste livro.
7. Reescreva a definição da classe DiaDoAno fornecida no Painel 7.3 de modo que as funções getNumeroMes e getDia sejam definidas inline.
Painel 7.5
1 2 3 4
Definições de funções inline ( parte 1 de 2 )
#include #include #include using namespace std;
Este é o Painel 7.4 reescrito utilizando funções-membros inline.
5 class BankAccount 6 { 7 public: 8 BankAccount(double balance, double rate); 9 BankAccount(int dollars, int cents, double rate); 10 BankAccount(int dollars, double rate); 11 BankAccount( ); void update( ); 12 13 void input( ); 14 void output( ) const; 15
double getBalance( ) const { return (accountDollars + accountCents*0.01);}
16
int getDollars( ) const { return accountDollars; }
17
int getCents( ) const { return accountCents; }
18
double getRate( ) const { return rate; }
19 20 21 22 23 24 25
void setBalance(double balance); void setBalance(int dollars, int cents); void setRate(double newRate); private: int accountDollars; //de saldo int accountCents; //de saldo double rate;//como uma porcentagem
26
int dollarsPart(double amount) const { return static_cast(amount); }
27
int centsPart(double amount) const;
28 29
int round(double number) const { return static_cast(floor(number + 0.5)); }
Mais Ferramentas
Painel 7.5
197
Definições de funções inline ( parte 2 de 2 )
double fraction(double percent) const { return (percent/100.0); } 30 31 };
■
MEMBROS ESTÁTICOS
Às vezes você quer ter uma variável que seja compartilhada por todos os objetos de uma classe. Por exemplo, você pode querer uma variável para contar o número de vezes que uma função-membro em particular é invocada por todos os objetos da classe. Tais variáveis são chamadas de variáveis estáticas e podem ser utilizadas para objetos da classe para se comunicarem uns com os outros ou coordenar suas ações. Tais variáveis permitem algumas das vantagens das variáveis globais sem abrir as comportas para todos os abusos a que as verdadeiras variáveis globais convidam. Em particular, uma variável estática pode ser privada, de modo que apenas objetos da classe têm acesso a ela diretamente. Se uma função não tem acesso aos dados de qualquer objeto e mesmo assim você quer que a função seja um membro da classe, você pode transformá-la em uma função estática. As funções estáticas podem ser invocadas do modo normal, por meio de um objeto da classe que faz a chamada. Entretanto, é mais comum e claro invocar uma função estática utilizando o nome da classe e o operador de resolução de escopo, como no seguinte exemplo: Servidor::getOrdemDeAtendimento( )
Já que uma função estática não precisa de um objeto que faz a chamada, a definição de uma função estática não pode utilizar nada que dependa de um objeto que faz a chamada. Uma definição de função estática não pode utilizar nenhuma variável não-estática nem funções membros não-estáticas, a não ser que a variável ou função nãoestática possua um objeto que faz a chamada que seja uma variável local ou algum objeto de outra forma criado na definição. Se esta última sentença parece difícil de entender, utilize apenas a regra mais simples de que a definição de uma função estática não pode utilizar nada que dependa de um objeto que faz a chamada. O Painel 7.6 é um programa de demonstração que emprega tanto variáveis estáticas quanto funções estáticas. Observe que as variáveis estáticas são indicadas pela palavra-chave qualificadora static no início de sua declaração. Observe também que todas as variáveis estáticas são inicializadas da seguinte forma: int Servidor::ordemDeAtendimento = 0; int Servidor::ultimoServido = 0; bool Servidor::abertoAgora = true;
Tal inicialização requer algumas explicações. Toda variável estática deve ser inicializada fora da definição de classe. Além disso, uma variável estática não pode ser inicializada mais de uma vez. Como no Painel 7.6, variáveis estáticas privadas — na realidade, todas as variáveis estáticas — são inicializadas fora da classe. Isso pode parecer contraditório à noção de privado. Entretanto, espera-se que o autor de uma classe faça as inicializações, normalmente no mesmo arquivo da definição de classe. Nesse caso, nenhum programador que utiliza a classe pode inicializar as variáveis estáticas, já que uma variável estática não pode ser inicializada uma segunda vez. Observe que a palavra-chave static é empregada na declaração da função-membro, mas não na definição da função-membro. Painel 7.6 1 2
Membros estáticos (parte 1 de 3)
#include using namespace std;
3 class Server 4 { 5 public: 6 Server(char letterName); 7 static int getTurn( ); void serveOne( ); 8
198
Construtores e Outras Ferramentas
Painel 7.6
Membros estáticos ( parte 2 de 3)
9 static bool stillOpen( ); 10 private: 11 static int turn; 12 static int lastServed; 13 static bool nowOpen; 14 char name; 15 }; 16 int Server:: turn = 0; 17 int Server:: lastServed = 0; 18 bool Server::nowOpen = true; 19 int main( ) 20 { 21 Server s1(’A’), s2(’B’); 22 int number, count; 23 do 24 { 25 cout << "Há quantos no seu grupo? "; 26 cin >> number; 27 cout << "A sua ordem de atendimento é: "; 28 for (count = 0; count < number; count++) 29 cout <
cout << "Agora o serviço é encerrado.\n";
35 return 0; 36 } 37 38 Server::Server(char letterName) : name(letterName) 39 {/*Propositadamente vazio*/} 40 int Server::getTurn( ) 41 { 42 turn++; 43 return turn; 44 } 45 bool Server::stillOpen( ) 46 { 47 return nowOpen; 48 }
Como getTurn é estático, apenas membros estáticos podem ser diferenciados aqui.
49 void Server::serveOne( ) 50 { 51 if (nowOpen && lastServed < turn) 52 { 53 lastServed++; 54 cout << "Servidor " << name 55 << " Agora está servindo " << lastServed << endl; 56 } 57 58 59 }
if (lastServed >= turn) //Todos foram servidos nowOpen = false ;
Mais Ferramentas
Painel 7.6
199
Membros estáticos ( parte 3 de 3)
DIÁLOGO PROGRAMA-USUÁRIO Há quantos no seu grupo? 3 A sua ordem de atendimento é: 1 2 3 O servidor A agora está servindo 1 O servidor B agora está servindo 2 Há quantos no seu grupo? 2 A sua ordem de atendimento é: 4 5 O servidor A agora está servindo 3 O servidor B agora está servindo 4 Há quantos no seu grupo? 0 A sua ordem de atendimento é: O servidor A agora está servindo 5 Agora o serviço será encerrado.
O programa no Painel 7.6 é o esboço de um cenário com uma fila de clientes esperando pelo serviço e dois servidores para atender esta única fila. Você pode imaginar diversos cenários de programação de sistema para transformar este exemplo em algo mais concreto. Para um modelo simples, só para aprender os conceitos, pense nos números produzidos por getOrdemDeAtendimento como aquelas senhas de papel entregues aos clientes de uma lanchonete ou sorveteria. Os dois servidores são, então, dois garçons. Um detalhe talvez peculiar desse estabelecimento é que os fregueses chegam em grupos, mas são atendidos um de cada vez (talvez para pedirem um tipo de sanduíche ou um sabor especial de sorvete).
8. A função definida da seguinte forma poderia ser acrescentada à classe Servidor no Painel 7.6 como uma função estática? Explique sua resposta. void Servidor::mostraSituacao( ) { cout << "Servindo agora " << ordemDeAtendimento << endl; cout << "o servidor de nome " << nome << endl; }
DEFINIÇÕES DE CLASSE ANINHADA E LOCAL
■
O material desta seção foi incluído para servir de referência e, a não ser por uma rápida referência no Capítulo 17, não é utilizado em nenhuma outra parte deste livro. Pode-se definir uma classe dentro de uma classe. Essa classe dentro de outra classe é chamada de classe aninhada . O esquema geral é óbvio: classe ClasseExterna
{ public:
... private: class ClasseInterna
{ ... }; ... };
Uma classe aninhada pode ser pública ou privada. Se for privada, como no nosso exemplo, não pode ser utilizada fora da classe externa. Quer a classe aninhada seja pública quer privada, pode ser usada em definições de funções-membros da classe externa.
200
Construtores e Outras Ferramentas
Como a classe aninhada está no escopo da classe externa, o nome da classe aninhada, como ClasseInterna em nosso exemplo, pode ser usado para algo mais fora da classe externa. Se a classe aninhada é pública, a classe aninhada pode ser usada como um tipo fora da classe externa. Entretanto, fora da classe externa, o nome do tipo da classe aninhada é ClasseExterna::ClasseInterna. Não teremos oportunidade de utilizar essas definições de classe aninhada neste livro. No entanto, no Capítulo 17 sugerimos uma possível aplicação para classes aninhadas. 2 Uma definição de classe também pode ser feita dentro de uma definição de função. Em tais casos, a classe é chamada de classe local, já que seu significado está confinado à definição de função. Uma classe local pode não conter membros estáticos. Não teremos a oportunidade de utilizar classes locais neste livro.
7.3
Vectors — Introdução à Standard Template Library — Tudo bem, eu vou comer — disse Alice —, e se ficar maior, poderei alcançar a chave; se ficar menor, passarei por baixo da porta. Assim, de qualquer forma entrarei no jardim. Lewis Carroll, Alice no País das Maravilhas
Vectors podem ser considerados vetores que podem crescer (e encolher) em comprimento enquanto o programa é executado. Em C++, assim que o programa cria um vetor, não pode alterar seu comprimento. Os vectors servem ao mesmo propósito que os vetores, a não ser pelo fato de que podem mudar de comprimento enquanto o programa é executado. Os vectors são formados a partir de uma classe modelo na Standard Template Library (STL). Falaremos de modelos no Capítulo 16 e da STL no Capítulo 19. Entretanto, é fácil aprender alguns usos básicos dos vectores antes de aprender sobre modelos e a STL em detalhe. Você não precisa saber muito sobre classes para utilizar vectors. Pode ler esta seção sobre vectors antes de ler o Capítulo 6. Não precisa ter lido as seções anteriores deste capítulo antes de ler esta seção. ■ FUNDAMENTOS DOS VECTORS
Como um vetor, um vector possui um tipo-base e armazena uma coleção de valores de seu tipo-base. Entretanto, a sintaxe para um tipo vector e uma declaração de variável vector é diferente da sintaxe dos vetores. Declare-se uma variável, v, para um vector com tipo-base int, da seguinte forma: vector v;
A notação vector é uma classe modelo (template) , o que quer dizer que você pode conectar qualquer tipo com o Tipo_Base e isso produzirá uma classe de vectors com esse tipo-base. Você pode pensar nisso simplesmente como a especificação do tipo-base para um vector no mesmo sentido em que se especifica um tipo-base para um vetor. Pode-se usar qualquer tipo, inclusive tipos-classe, como o tipo-base para um vector. A notação vector é um nome de classe e, assim, a declaração anterior de v como um vector de tipo vector inclui uma chamada ao construtor-padrão da classe vector que cria um vector objeto vazio (sem elementos). Os elementos dos vectors são indexados a partir do 0, como acontece com os vetores. A notação de colchetes pode ser usada para ler ou alterar esses elementos, exatamente como com um vetor. Por exemplo, a linha seguinte altera o valor do iésimo elemento do vector v e, depois, apresenta como saída esse valor alterado. ( i é uma variável int.) v[i] = 42; cout << "A resposta é " << v[i];
Existe, todavia, uma restrição quanto ao uso da notação de colchetes com vectors que a torna diferente da mesma notação utilizada com vetores. Pode-se usar v[i] para mudar o valor do iésimo elemento. Entretanto, não se pode inicializar o iésimo elemento usando v[i]; só se pode mudar um elemento que já tenha recebido algum valor. Para acrescentar um elemento a um vector pela primeira vez, normalmente se utiliza a função-membro push_back.
2.
A sugestão está na subseção do Capítulo 17 intitulada "Classes Amigas e Alternativas Similares".
Vectors — Introdução à Standard Template Library
201
Acrescentam-se elementos a um vector na ordem de posições, primeiro na posição 0, depois na posição 1, depois 2, e assim por diante. A função-membro push_back acrescenta um elemento na próxima posição disponível. Por exemplo, o trecho seguinte fornece valores iniciais aos elementos 0, 1 e 2 do vector exemplo: vector exemplo; exemplo.push_back(0.0); exemplo.push_back(1.1); exemplo.push_back(2.2);
O número de elementos em um vector é chamado de tamanho do vector. O tamanho da função-membro pode ser usado para determinar quantos elementos existem em um vector. Por exemplo, depois que o código anteriormente exibido tiver sido executado, exemplo.size( ) apresenta como saída 3. Você pode mandar escrever todos os elementos atualmente no vector exemplo da seguinte forma: for (int i = 0; i < exemplo.size( ); i++)
cout << exemplo[i] << endl;
A função size retorna um valor de tipo unsigned int, não um valor de tipo int. Esse valor retornado deve ser automaticamente convertido no tipo int quando precisar ser de tipo int, mas alguns compiladores talvez o avisem de que você está usando um unsigned int em um local em que se exige um int. Se você quiser segurança, pode aplicar sempre um casting de tipo para converter o unsigned int retornado em int, ou, em casos como o deste loop for, utilizar uma variável de controle no loop de tipo unsigned int, assim: for (unsigned int i = 0; i < exemplo.size( ); i++)
cout << exemplo[i] << endl;
O Painel 7.7 apresenta uma demonstração simples que ilustra algumas técnicas básicas de vectors. Há um construtor de vectors que requer um argumento de número inteiro e inicializa o número de posições dado como o argumento. Por exemplo, se você declarar v da seguinte forma: vector v(10);
os primeiros dez elementos são inicializados com o valor 0, e v.size( ) apresentará como saída 10. Então, você pode fixar o valor do iésimo elemento utilizando v[i] para valores de i iguais a 0 até 9. Em particular, o trecho seguinte pode se seguir imediatamente à declaração: for (unsigned int i = 0; i < 10; i++)
v[i] = i; Painel 7.7
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
Utilizando um vector ( parte 1 de 2) #include #include using namespace std; int main( )
{ vector v; cout << "Forneça uma lista de números positivos.\n" << "Coloque um número negativo ao final.\n"; int next;
cin >> next; while (next > 0)
{ v.push_back(next); cout << next << " acrescentado. "; cout << "v.size( ) = " << v.size( ) << endl; cin >> next; } cout << "Você digitou:\n";
202
Construtores e Outras Ferramentas
Painel 7.7
Utilizando um vector ( parte 2 de 2)
19 20 21
for (unsigned int i = 0; i < v.size( ); i++)
22 23 }
return 0;
cout << v[i] << " "; cout << endl;
DIÁLOGO PROGRAMA-USUÁRIO Forneça uma lista de números positivos. Coloque um número negativo ao final. 2 4 6 8 -1
2 acrescentado. 4 acrescentado. 6 acrescentado. 8 acrescentado. Você digitou: 2 4 6 8
v.size v.size v.size v.size
= = = =
1 2 3 4
Para fixar o iésimo elemento para i maior ou igual a 10, você utilizaria push_back. Quando se usa o construtor com um inteiro como argumento, os vectors de números são inicializados com o valor zero do tipo do número. Se o tipo-base do vector é um tipo-classe, o construtor-padrão é utilizado para a inicialização. A definição de vector é dada na biblioteca vector, que a coloca na std namespace. Assim, um arquivo que utiliza vectors incluiria o seguinte (ou algo similar): #include using namespace std; VECTORS Vectors são utilizados de forma semelhante aos vetores, mas um vector não possui um tamanho fixo. Se for necessária uma capacidade maior para armazenar outro elemento, sua capacidade é aumentada automaticamente. Os vectors são definidos na biblioteca vector , que os coloca no std namespace . Assim, um arquivo que utiliza vectors incluiria as seguintes linhas: #include using namespace std; A classe de vector para um dado Tipo_Base é escrita como vector. Dois exemplos de declarações de vector: vector< int> v; //construtor-padrão produzindo um vector vazio. vector registro(20); //construtor de vector utiliza o //construtor-padrão para UmaClasse para inicializar 20 elementos. Os elementos são acrescentados a um vector por meio da função-membro push_back , como ilustrado a seguir: v.push_back(42); Uma vez que uma posição de elemento tenha recebido seu primeiro elemento, com push_back ou com uma inicialização via construtor, pode-se ter acesso a essa posição de elemento por meio da notação de colchetes, exatamente como com os elementos de vetores.
UTILIZANDO COLCHETES ALÉM DO TAMANHO DO VECTOR Se v é um vector e i é maior ou igual a v.size( ) , elemento v[i] ainda não existe e precisa ser criado por meio de push_back para acrescentar elementos até incluir a posição i. Se você tentar fixar v[i] para um i maior ou igual a v.size( ) , como em v[i] = n; você pode receber ou não uma mensagem de erro, mas seu programa, infalivelmente, se comportará mal em algum momento.
Vectors — Introdução à Standard Template Library
203
ATRIBUIÇÃO DE VECTORS É BEM COMPORTADA O operador de atribuição com vectors faz uma atribuição elemento-a-elemento ao vector no lado esquerdo do operador de atribuição (aumentando a capacidade, se necessário, e atualizando o tamanho do vector no lado esquerdo do operador de atribuição). Assim, desde que o operador de atribuição sobre o tipo-base faça uma cópia independente de um elemento do tipo-base, o operador de atribuição sobre o vector fará uma cópia independente, não um alias (nome alternativo), do vector no lado direito do operador de atribuição. Observe que, para o operador de atribuição produzir uma cópia totalmente independente do vector no lado direito do operador de atribuição, é necessário que o operador de atribuição sobre o tipo-base faça cópias completamente independentes. O operador de atribuição sobre um vector é apenas tão bom (ou ruim) quanto o operador de atribuição sobre seu tipo-base.
■
QUESTÕES DE EFICIÊNCIA
Em determinado momento um vector possui uma capacidade, que é o número de elementos para os quais há memória alocada naquele momento. A função-membro capacity( ) pode ser usada para descobrir a capacidade de um vector. Não confunda a capacidade de um vector com o tamanho de um vector. O tamanho é o número de elementos no vector, enquanto a capacidade é o número de elementos para os quais há memória alocada. A capacidade em geral é maior que o tamanho, e a capacidade é sempre maior ou igual ao tamanho. Sempre que um vector esgota sua capacidade e necessita de espaço para um membro adicional, a capacidade é automaticamente aumentada. A quantidade exata do aumento depende da implementação, mas sempre proporciona uma capacidade maior do que a imediatamente necessária. Um esquema de implementação comumente usado faz com que a capacidade dobre sempre que necessitar de aumento. Como aumentar a capacidade é uma tarefa complexa, esse método de realocação de capacidade em grandes blocos é mais eficiente que alocar diversos blocos pequenos. Pode-se ignorar completamente a capacidade de um vector, e isso não terá nenhum efeito sobre o que seu programa faz. Entretanto, se a eficiência estiver em questão, você mesmo pode querer controlar a capacidade e não simplesmente aceitar o comportamento-padrão de dobrar a capacidade sempre que se precisa aumentá-la. Você pode utilizar a função-membro reserve para aumentar explicitamente a capacidade de um vector. Por exemplo, v.reserve(32);
fixa a capacidade em pelo menos 32 elementos e v.reserve(v.size( ) + 10);
fixa a capacidade em pelo menos mais 10 do que o número de elementos atualmente no vector. Observe que você pode confiar em v.reserve para aumentar a capacidade de um vector, mas não necessariamente para diminuir a capacidade de um vector se o argumento for menor que a capacidade atual. Você pode alterar o tamanho de um vector utilizando a função-membro resize. Por exemplo, a linha seguinte altera o tamanho de um vector para 24 elementos: v.resize(24);
Se o tamanho anterior fosse menor que 24, os novos elementos seriam inicializados como descrevemos para o construtor com um inteiro como argumento. Se o tamanho anterior fosse maior que 24, todos, exceto os primeiros 24 elementos, seriam perdidos. A capacidade é automaticamente aumentada se necessário. Utilizando resize e reserve, você pode diminuir o tamanho e a capacidade de um vector quando não houver mais necessidade de alguns elementos ou de alguma capacidade.
TAMANHO E CAPACIDADE O tamanho de um vector é o número de elementos no vector. A capacidade de um vector é o número de elementos para os quais existe memória alocada no momento. Para um vector v, o tamanho e a capacidade podem ser recuperados com as funções-membros v.size( ) e v.capacity( ).
204
Construtores e Outras Ferramentas
9. O programa seguinte é legal? Se for, qual é a saída? #include #include using namespace std; int main( )
{
vector v(10); int i; for (i = 0; i < v.size( ); i++)
v[i] = i;
vector copia; copy = v; v[0] = 42; for (i = 0; i < copia.size( ); i++)
cout << copia[i] << " "; cout << endl; return 0;
} 10. Qual é a diferença entre o tamanho e a capacidade de um vector?
■
■
■
■
■ ■ ■
Um construtor é uma função-membro de uma classe chamada automaticamente quando um objeto da classe é declarado. Um construtor deve ter o mesmo nome da classe da qual é membro. Um construtor-padrão é um construtor sem parâmetros. Você deve sempre definir um construtor-padrão para suas classes. Uma variável-membro para uma classe pode ela mesma ser de um tipo-classe. Se uma classe tiver uma variável-membro classe, o construtor da variável-membro pode ser invocado na seção de inicialização do construtor da classe exterior. Um parâmetro constante chamado por referência é mais eficiente que um parâmetro chamado por valor para parâmetros de tipo-classe. Transformar em inline definições de função bastante curtas pode aumentar a eficiência do seu código. Variáveis-membros estáticas são variáveis compartilhadas por todos os objetos de uma classe. Classes vector possuem objetos que se comportam de maneira bem semelhante a vetores cuja capacidade de abrigar elementos aumenta automaticamente quando é necessária uma capacidade maior.
RESPOSTAS DOS EXERCÍCIOS DE AUTOTESTE
1.
SuaClasse umObjeto(42, ’A’); //LEGAL SuaClasse outroObjeto; //LEGAL SuaClasse maisOutroObjeto( ); //PROBLEMA umObjeto = SuaClasse(99, ’B’); //LEGAL umObjeto = SuaClasse( ); //LEGAL umObjeto = SuaClasse; //ILEGAL A linha marcada com //PROBLEMA não é estritamente
ilegal, mas provavelmente não significa o que você pensa. Se você pensa que é uma declaração de um objeto chamado maisOutroObjeto, está errado. É uma declaração correta de uma função chamada maisOutroObjeto que requer zero argumentos e retorna um
Respostas dos Exercícios de Autoteste
205
valor de tipo SuaClasse, mas normalmente este não é o significado pretendido. Para valores práticos, tal vez você deva considerá-la ilegal. A forma correta de declarar um objeto chamado maisOutroObjeto de forma a inicializá-lo com o construtor-padrão é a seguinte: SuaClasse maisOutroObjeto;
2. Um construtor-padrão é um construtor que não requer argumentos. Nem toda classe possui um construtor-padrão. Se você não definir nenhum construtor para uma classe, um construtor-padrão será automaticamente providenciado. Por outro lado, se você definir um ou mais construtores, mas não definir um construtor-padrão, sua classe não terá construtor-padrão. 3. A definição é mais fácil de dar se você também acrescentou uma função de ajuda privada chamada ContaBancaria::digitoParaInt, como a seguir, à classe ContaBancaria. //Utiliza iostream: void ContaBancaria::entrada( ) { int reais; char ponto, digito1, digito2; cout << "Informe o saldo bancário (inclua centavos mesmo se for ,00) R$"; cin >> reais; cin >> ponto >> digito1 >> digito2; contaReais = reais; contaCentavos = digitoParaInt(digito1)*10 + digitoParaInt(digito2); if (contaReais < 0) contaCentavos = -contaCentavos; cout << "Informe a taxa de juros (SEM o sinal de porcentagem): "; cin >> taxa; setTaxa(taxa); } int ContaBancaria::digitoParaInt(char digito) { return (static_cast(digit) - static_cast(’0’)); }
4. A função-membro entrada altera o valor do objeto que faz a chamada, e assim o compilador emitirá uma mensagem de erro se você acrescentar o modificador const. 5. Similaridades: cada método de chamada de parâmetro protege o argumento do que chama (caller ) de mudanças. Diferenças: se o tipo é uma grande estrutura ou objeto de classe, uma chamada por valor faz uma cópia do argumento daquele que chama e, assim, utiliza mais memória que uma chamada por referência constante. 6. Em const int x = 17 ;, a palavra-chave const promete ao compilador que o código escrito pelo autor não alterará o valor de x. Na declaração int f( ) const;, a palavra-chave const é uma promessa ao compilador de que o código escrito pelo autor para implementar a função f não alterará nada no objeto que faz a chamada. Em int g(const A& x); , a palavra-chave const é uma promessa ao compilador de que o código escrito pelo autor da classe não alterará o argumento conectado com x. class DiaDoAno 7. { public: DiaDoAno(int valorDia, int valorMes); DiaDoAno(int valorMes); DiaDoAno( ); void entrada( ); void saida( ); int getNumeroMes( ) { return mes; } int getDia( ) { return dia; } private: int dia;
206
Construtores e Outras Ferramentas
int mes; void testeData(
);
};
8. Não, não pode ser uma função-membro estática porque requer um objeto que faz a chamada para o nome da variável-membro. 9. O programa é legal. A saída é 0 1 2 3 4 5 6 7 8 9
Observe que alterar v não altera copia. Uma verdadeira cópia independente é feita com a atribuição. copia = v;
10. O tamanho é o número de elementos em um vector, enquanto a capacidade é o número de elementos para os quais há memória alocada. Em geral, a capacidade é maior que o tamanho.
PROJETOS DE PROGRAMAÇÃO 1. Defina uma classe chamada Mes que é um tipo de dado abstrato para um mês. Sua classe terá uma variá vel-membro de tipo int para representar um mês (1 para janeiro, 2 para fevereiro, e assim por diante). Inclua todas as seguintes funções-membros: um construtor para estabelecer o mês utilizando as primeiras três letras do nome do mês como três argumentos, um construtor para estabelecer o mês utilizando um inteiro como argumento (1 para janeiro, 2 para fevereiro, e assim por diante), um construtor-padrão, uma função de entrada que leia o mês como um inteiro, uma função de entrada que leia o mês como as primeiras três letras do nome do mês, uma função de saída que retorne o mês como um inteiro, uma função de saída que retorne o mês como as três primeiras letras do nome do mês e uma função-membro que retorne o mês seguinte como um valor do tipo Mes. Insira sua definição de classe em um programa-teste. 2. Redefina a implementação da classe Mes descrita no Projeto de Programação 1 (ou faça a definição pela primeira vez, mas faça a implementação como descrita aqui). Desta vez, o mês é implementado como três variáveis-membros de tipo char que armazenam as três primeiras letras do nome do mês. Insira sua definição em um programa-teste. 3. Minha mãe sempre levava um pequeno contador vermelho quando ia à mercearia. O contador era usado para calcular a quantia de dinheiro que ela deveria pagar se comprasse tudo o que havia colocado na cesta. O contador possuía um painel com quatro dígitos, botões de incremento para cada dígito e um botão de reinício. Um indicador de alerta ficava vermelho se ultrapassasse os R$ 99,99 que ele poderia registrar. (Isso foi há muito tempo.) Escreva e implemente as funções-membros de uma classe Contador que simule e generalize, até certo ponto, o comportamento desse contador. O construtor deve criar um objeto Contador que pode contar até o argumento do construtor. Ou seja, Contador(9999) deve proporcionar um contador que pode contar até 9999. Um contador recentemente construído exibe uma leitura de 0. A função-membro void reinicia( ); faz com que o contador retorne ao 0. A função-membro void incr1( ); incrementa o dígito da unidade em 1, void incr10( ); incrementa o dígito das dezenas em 1 e void incr100( ); e void incr1000( ); incrementam os próximos dois dígitos. O acréscimo de qualquer taxa durante o incremento não deve exigir nenhuma outra ação além de adicionar um número adequado aos membros dados privados. Uma função-membro bool ultrapassou( ); detecta quando o máximo foi ultrapassado. (A ultrapassagem é o resultado de incrementar os membros dados privados do contador além do máximo estabelecido na construção do contador.) Utilize esta classe para fornecer uma simulação do pequeno contador vermelho da minha mãe. Embora o display seja um inteiro, na simulação os dois dígitos mais à direita (a ordem seguida é dos menores para os maiores) são sempre considerados centavos e dezenas de centavos, o próximo dígito é dos reais e o quarto dígito é das dezenas de reais. Forneça teclas para centavos, moedas de dez centavos, reais e notas de dez reais. Infelizmente, nenhuma escolha de teclas parece especialmente mnemônica. Uma opção é utilizar as teclas asdfo: a para os centa vos, seguida por um dígito de 1 a 9; s para as moedas de dez centavos, seguida por um dígito de 1 a 9; d para os reais, seguida por um dígito de 1 a 9; e f para as notas de dez reais, mais uma vez seguida por um dígito de 1 a 9. Cada entrada (uma das asdf seguida por um dígito de 1 a 9) é seguida pelo pressionamento da tecla Return. Quando o valor ultrapassar o máximo, isso será informado após cada operação. Pode-se consultar o valor de ultrapassagem com a tecla o.
Sobrecarga de Operador, Amigos e Referências Sobrecarga de Operador, Amigos e Referências
Capítulo 8Sobrecarga de Operador, Amigos e Referências As verdades eternas não serão nem verdades nem eternas se não tiverem um significa- do novo a cada nova situação social. Franklin D. Roosevelt, Palestra na Universidade da Pensilvânia [20 de setembro de 1940]
INTRODUÇÃO Este capítulo trata de diversas ferramentas para serem utilizadas na definição de classes. A primeira ferramenta é a sobrecarga de operador, que permite que se sobrecarregue operadores, como + e ==, de modo que se apliquem a objetos das classes que você define. A segunda ferramenta é a utilização de funções amigas, funções que não são membros de uma classe, mas continuam tendo acesso a membros privados da classe. Este capítulo também discute como fornecer conversão automática de tipo de outros tipos de dados para as classes que você define. Se você ainda não leu sobre vetores (Capítulo 5), deve pular a subseção da seção 8.3 intitulada Sobrecarregando o Operador Vetor [ ], pois aborda um assunto que não fará sentido para você que ainda não conhece os fundamentos dos vetores.
8.1
Fundamentos da Sobrecarga de Operador Ele é um operador suave. Citação de uma canção de Sade (letra de Sade Adu e Ray St. John)
Operadores como +, -, %, == e outros não são nada além de funções utilizadas com uma sintaxe levemente diferente. Escrevemos x + 7 em vez de +(x, 7), mas o operador + é uma função que requer dois argumentos (freqüentemente chamados de operandos em vez de argumentos) e retorna um único valor. Dessa forma, os operadores não são, na realidade, necessários. Poderíamos utilizar +(x, 7) ou mesmo add(x, 7). Os operandos são um exemplo do que muitas vezes chamamos de açúcar sintático: uma sintaxe levemente diferente e muito apreciada pelas pessoas. Entretanto, as pessoas se sentem muito à vontade com a sintaxe normal dos operadores, x + 7, que o C++ utiliza para tipos como int e double. E uma linguagem de alto nível, como C++, é também uma forma de tornar as pessoas à vontade com a programação de computadores. Assim, esse açúcar sintático provavelmente é uma boa idéia; pelo menos, é uma idéia bastante defendida. Em C++, podem-se sobrecarregar os operadores, como + e ==, para que funcionem com operandos nas classes definidas. O modo de sobrecarregar um operador é bastante similar ao de se sobrecarregar um nome de função. Os detalhes ficarão mais claros com um exemplo.
208
Sobrecarga de Operador, Amigos e Referências
■ FUNDAMENTOS DA SOBRECARGA
O Painel 8.1 contém a definição de uma classe cujos valores são quantias monetárias, como R$ 9,99 ou R$ 1.567,29. A classe tem muito em comum com a classe ContaBancaria que definimos no Painel 7.2. Representa quantias monetárias da mesma forma, com dois ints para os reais e centavos. Possui as mesmas funções de ajuda privadas. Seus construtores e funções de acesso e mutantes são similares aos da classe ContaBancaria. O que é realmente novo nessa classe Dinheiro é que sobrecarregamos os sinais de mais e de menos para que pudessem ser usados para adicionar ou subtrair dois objetos da classe Dinheiro a fim de verificar se eles representam a mesma quantia em dinheiro. Vamos dar uma olhada nesses operadores sobrecarregados. Pode-se sobrecarregar o operador + (e muitos outros operadores) para que aceite argumentos de um tipo-classe. A diferença entre sobrecarregar o operador + e definir uma função ordinária envolve apenas uma leve mudança na sintaxe: utiliza-se o símbolo + como o nome da função e coloca-se a palavra-chave operator antes do +. A declaração do operador (declaração de função) para o sinal de mais é a seguinte: const Dinheiro operator +(const Dinheiro& quantia1, const Dinheiro& quantia2);
Os dois operandos (argumentos) são parâmetros de referência constantes do tipo Dinheiro. Os operandos podem ser de qualquer tipo, desde que pelo menos um seja um tipo-classe. No caso geral, os operandos podem ser parâmetros chamados por valor ou por referência e podem ter o modificador const ou não. Entretanto, por razões de eficiência, chamadas constantes por referência normalmente são usadas em lugar de chamadas por valor para classes. Nesse caso, o valor retornado é do tipo Dinheiro, mas, no caso geral, o valor retornado pode ser de qualquer tipo, inclusive void. O const antes do tipo retornado Dinheiro será explicado posteriormente neste capítulo. Por enquanto, você pode ignorá-lo sem problemas. Observe que os operadores binários sobrecarregados + e - não são operadores-membros (funções-membros) da classe Dinheiro e, portanto, não têm acesso aos membros privados da classe Dinheiro. Este é o motivo por que a definição para os operadores sobrecarregados utiliza funções de acesso e mutantes. Neste mesmo capítulo veremos outras formas de sobrecarregar um operando, inclusive como um operador-membro. Cada uma das diferentes formas de sobrecarregar um operador tem suas vantagens e desvantagens. As definições dos operadores binários sobrecarregados + e - talvez sejam um pouco mais complicadas do que você poderia esperar. Os detalhes extras estão aí para lidar com o fato de que quantias monetárias podem ser negativas. O operador unário negativo - é discutido na subseção Sobrecarregando Operadores Unários, neste mesmo capítulo. O operador == também é sobrecarregado para poder ser usado na comparação de dois objetos da classe Dinheiro. Observe que o tipo retornado é bool, de modo que == possa ser usado para fazer comparações nas formas usuais, como um comando if-else. Painel 8.1
1 2 3 4
Sobrecarregando um operador ( parte 1 de 5)
#include #include #include using namespace std;
5 //Classe para quantias de dinheiro pelo valor atual no mercado norte-americano 6 class Money 7 { 8 public: 9 Money( ); 10 Money(double amount); 11 Money(int theDollars, int theCents); 12 Money(int theDollars); 13 double getAmount( ) const; int getDollars( ) const; 14 int getCents( ) const; 15 16 void input( ); //Lê o símbolo do dólar e a quantia. 17 void output( ) const;
Fundamentos da Sobrecarga de Operador Painel 8.1
209
Sobrecarregando um operador ( parte 2 de 5)
18 private: 19 int dollars; //Uma quantia negativa é representada como dólares e centavos negativos. 20 int cents; //Por exemplo, $4.50 negativo fica accountDollars como -4 e a ccountCents como -50. 21 int dollarsPart(double amount) const ; 22 int centsPart(double amount) const ; 23 int round(double number) const ; 24 }; 25 const Money operator +(const Money& amount1, const Money& amount2); Este é o operador únario e é discutido na subseção Sobrecarregando Operadores Unários.
26 const Money operator -(const Money& amount1, const Money& amount2); 27 bool operator ==(const Money& amount1, const Money& amount2); 28 const Money operator -(const Money& amount); 29 int main( ) 30 { 31 Money yourAmount, myAmount(10, 9); 32 cout << "Informe a quantia: "; 33 yourAmount.input( ); 34 cout << "A sua quantia é "; 35 yourAmount.output( ); 36 cout << endl; 37 cout << "Minha quantia é "; 38 myAmount.output( ); 39 cout << endl; 40 if (yourAmount == myAmount) 41 cout << "Nós temos a mesma quantia.\n"; 42 else 43 cout << "Um de nós é mais rico.\n"; 44 45 46 47 48 49
Para uma explicação sobre um const em um tipo fornecido, veja a subseção Retornando um valor const.
Money ourAmount = yourAmount + myAmount; yourAmount.output( ); cout << " + "; myAmount.output( ); cout << " equals "; ourAmount.output( ); cout << endl; Money diffAmount = yourAmount - myAmount; yourAmount.output( ); cout << " - "; myAmount.output( ); cout << " equals "; diffAmount.output( ); cout << endl;
50 return 0; 51 } 52 const Money operator +(const Money& amount1, const Money& amount2) 53 { 54 int allCents1 = amount1.getCents( ) + amount1.getDollars( )*100; 55 int allCents2 = amount2.getCents( ) + amount2.getDollars( )*100; 56 int sumAllCents = allCents1 + allCents2; 57 int absAllCents = abs(sumAllCents); //O dinheiro pode ser negativo. 58 int finalDollars = absAllCents/100; 59 int finalCents = absAllCents%100; 60 61 62 63 64
if (sumAllCents < 0)
{ finalDollars = -finalDollars; finalCents = -finalCents; }
Observe que precisamos utilizar funções de acesso e mutante.
210
Sobrecarga de Operador, Amigos e Referências
Painel 8.1
65 66 67 68 69 70 71 72 73 74 75
Sobrecarregando um operador ( parte 3 de 5)
return Money(finalDollars, finalCents); } //Utiliza cstdlib: const Money operator -(const Money& amount1, const Money& amount2) { int allCents1 = amount1.getCents( ) + amount1.getDollars( )*100; int allCents2 = amount2.getCents( ) + amount2.getDollars( )*100; int diffAllCents = allCents1 - allCents2; int absAllCents = abs(diffAllCents); int finalDollars = absAllCents/100; int finalCents = absAllCents%100;
76 77 78 79 80
if (diffAllCents < 0)
81 82 }
return Money(finalDollars, finalCents);
Se você ficar intrigado com as declarações de return, veja a dica com o título Um Construtor Pode Retornar um Objeto.
{ finalDollars = -finalDollars; finalCents = -finalCents; }
83 bool operator ==(const Money& amount1, const Money& amount2) 84 { 85 return ((amount1.getDollars( ) == amount2.getDollars( )) 86 && (amount1.getCents( ) == amount2.getCents( ))); 87 } 88 const Money operator -(const Money& amount) 89 { 90 return Money(-amount.getDollars( ), -amount.getCents( )); 91 } Se você preferir, pode transformar essas definições de construtor curtas em definições inline, como explicaremos no Capítulo 7.
92 93
Money::Money( ): dollars(0), cents(0) {/*Corpo propositadamente vazio.*/}
94 95 96
Money::Money(double amount) : dollars(dollarsPart(amount)), cents(centsPart(amount)) {/*Corpo propositadamente vazio*/}
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111
Money::Money(int theDollars) : dollars(theDollars), cents(0) {/*Corpo propositadamente vazio*/} //Utiliza cstdlib: Money::Money(int theDollars, int theCents) { if ((theDollars < 0 && theCents > 0) || (theDollars > 0 && theCents < 0)) { cout << "Dados monetários inconsistentes.\n"; exit(1); } dollars = theDollars; cents = theCents; }
112 double Money::getAmount( ) const 113 { 114 return (dollars + cents*0.01);
Fundamentos da Sobrecarga de Operador Painel 8.1
Sobrecarregando um operador ( parte 4 de 5)
115 } 116 int Money::getDollars( ) 117 { 118 return dollars; 119 } 120 int Money::getCents( ) 121 { 122 return cents; 123 }
const
const
124 //Utiliza iostream e cstdlib: 125 void Money::output( ) const 126 { 127 int absDollars = abs(dollars); 128 int absCents = abs(cents); 129 if (dollars < 0 || cents < 0)//trata do caso em que dólares == 0 ou centavos == 0 130 cout << "$-"; 131 else 132 cout << ’$’; 133 cout << absDollars; 134 135 if (absCents >= 10) 136 cout << ’.’ << absCents; 137 else 138 cout << ’.’ << ’0’ << absCents; 139 } 140 //Utiliza iostream e cstdlib: Para uma melhor definição da função input, 141 void Money::input( ) veja Exercício de Autoteste 3 no Capítulo 7. 142 { 143 char dollarSign; 144 cin >> dollarSign; //esperamos que sim 145 if (dollarSign != ’$’) 146 { 147 cout << "Não há o símbolo do dólar na entrada Money.\n"; 148 exit(1); 149 } 150 151 152 153 154 }
double amountAsDouble;
cin >> amountAsDouble; dollars = dollarsPart(amountAsDouble); cents = centsPart(amountAsDouble);
155 156 157 158
int Money::dollarsPart(double amount) const
159 160
int Money::round(double number) const
int Money::centsPart(double amount) const
211
212
Sobrecarga de Operador, Amigos e Referências
Painel 8.1
Sobrecarregando um operador ( parte 5 de 5)
DIÁLOGO PROGRAMA-USUÁRIO Informe a quantia: $123.45 A sua quantia é $123.45 Minha quantia é $10.09. Um de nós é mais rico. $123.45 + $10.09 igual a $133.54 $123.45 - $10.09 igual a $113.36
Se você observar a função main no programa de demonstração no Painel 8.1, verá que os operadores binários sobrecarregados +, - e ==, são usados com objetos da classe Dinheiro da mesma forma que +, - e == são usados com os tipos predefinidos, como int e double. Pode-se sobrecarregar a maioria dos operadores, mas não todos. Uma forte restrição sobre a sobrecarga de um operador é que pelo menos um operando deve ser de um tipo classe. Assim, por exemplo, pode-se sobrecarregar o operador % para aplicá-lo a dois objetos de tipo Dinheiro ou a um objeto de tipo Dinheiro e um double, mas não se pode sobrecarregar % para combinar dois doubles. SOBRECARGA DE OPERADOR
Um operador (binário), como +, -, /, %, etc., é apenas uma função que é chamada com uma sintaxe diferente para a listagem de seus argumentos. Com um operador binário, os argumentos são listados antes e depois do operador; com uma função, os argumentos são listados entre parênteses, depois do nome da função. Uma definição de operador é escrita de modo similar a uma definição de função, a não ser pelo fato de que a definição do operador inclui a palavra reservada operator antes do nome do operador. Os operadores predefinidos, como +, -, etc., podem ser sobrecarregados recebendo uma nova definição para um tipo classe. Um exemplo de sobrecarga de +, - e == é fornecido no Painel 8.1.
UM CONSTRUTOR PODE RETORNAR UM OBJETO
Muitas vezes pensamos em um construtor como se fosse uma função void. Entretanto, os construtores são funções especiais com propriedades especiais, e às vezes faz mais sentido pensar neles como retornando um valor. Observe o comando return na definição do operador sobrecarregado + no Painel 8.1, que repetimos a seguir: return Dinheiro(finalReais, finalCentavos);
A expressão fornecida é uma invocação do construtor para Dinheiro. Embora às vezes pensemos em um construtor como em uma função void, um construtor constrói um objeto e também se pode pensar que forneça um objeto da classe. Se você não se sente à vontade com esse uso do construtor, talvez ajude saber que esse comando return equivale ao seguinte código, mais intrincado e menos eficiente: Dinheiro temp; temp = Dinheiro(finalReais, finalCentavos); return temp;
Geralmente se chama uma expressão como Dinheiro(finalReais, finalCentavos) de objeto anônimo, já que não é nomeado por nenhuma variável. Entretanto, continua sendo um objeto completo. Você pode até usá-lo como objeto que faz a chamada, como na seguinte expressão: Dinheiro(finalReais, finalCentavos).getReais( )
A expressão anterior retorna o valor
int
de
finalReais .
1. Qual é a diferença entre um operador (binário) e uma função? 2. Suponha que você deseje sobrecarregar o operador < para aplicá-lo ao tipo Dinheiro definido no Painel 8.1. O que você precisa acrescentar à definição de Dinheiro fornecida no Painel 8.1? 3. É possível utilizar a sobrecarga de operador para mudar o comportamento de + sobre inteiros? Por que sim ou por que não?
Fundamentos da Sobrecarga de Operador
213
■ RETORNANDO UM VALOR const
Observe os tipos retornados nas declarações para operadores sobrecarregados da classe Dinheiro no Painel 8.1. Por exemplo, a seguinte declaração do operador positivo sobrecarregado, como aparece no Painel 8.1: const Dinheiro operator +(const Dinheiro& quantia1, const Dinheiro& quantia2);
Esta subseção explica o const no início da linha. Mas antes de falarmos deste primeiro const, vamos ter certeza de que entendemos todos os outros detalhes sobre o fornecimento de um valor. Assim, vamos primeiro considerar o caso em que const não aparece na declaração nem na definição do operador positivo sobrecarregado. Suponhamos que a declaração fosse a seguinte: Dinheiro operator +(const Dinheiro& quantia1, const Dinheiro& quantia2);
e vejamos o que podemos fazer com o valor retornado. Quando um objeto é retornado, por exemplo, (m1 + m2), em que m1 e m2 são do tipo Dinheiro, o objeto pode ser usado para invocar uma função-membro, que pode alterar ou não o valor das variáveis-membros no ob jeto (m1 + m2). Por exemplo, (m1 + m2).saida( );
é perfeitamente legal. Nesse caso, não altera o objeto (m1 + m2). Não obstante, se omitirmos o const antes do tipo retornado pelo operador positivo, a linha seguinte seria legal e alteraria os valores das variáveis-membros do objeto (m1 + m2): (m1 + m2).entrada( );
Assim, os objetos podem ser alterados, mesmo quando não estão associados a qualquer variável. Uma maneira de se entender isso é observar que os objetos possuem variáveis-membros e, dessa forma, possuem alguns tipos de variáveis que podem ser alteradas. Agora vamos presumir que tudo é como mostrado no Painel 8.1; ou seja, há um const antes do tipo retornado de cada operador que retorna um objeto de tipo Dinheiro. Por exemplo, a seguir está a declaração do operador positivo sobrecarregado como aparece no Painel 8.1: const Dinheiro operator +(const Dinheiro& quantia1, const Dinheiro& quantia2);
O primeiro const na linha é um novo uso do modificador const. Isso se chama retornando um valor como const ou retornando um valor const. O que o modificador const significa, nesse caso, é que o objeto retornado não pode ser alterado. Por exemplo, considere o seguinte código: Dinheiro m1(10.99), m2(23.57); (m1 + m2).saida( );
A invocação de saida é perfeitamente legal, porque não altera o objeto (m1 + m2). No entanto, com aquele const antes do tipo retornado, o código seguinte produzirá uma mensagem de erro do compilador: (m1 + m2).entrada( );
Por que você iria querer retornar um valor const? Porque ele proporciona um tipo de verificação automática de erros. Quando você constrói (m1 + m2), não deseja alterá-lo inadvertidamente. A princípio, essa proteção contra a mudança de um objeto pode parecer exagerada, já que se pode ter Dinheiro m3; m3 = (m1 + m2);
e você pode muito bem querer mudar m3. Não há problema — o código seguinte é perfeitamente legal: m3 = (m1 + m2); m3.entrada( );
Os valores de m3 e (m1 + m2) são dois objetos diferentes. O operador de atribuição não torna m3 o mesmo que o objeto (m1 + m2). Em vez disso, copia os valores das variáveis-membros de (m1 + m2) nas variáveis-mem-
214
Sobrecarga de Operador, Amigos e Referências
bros de m3. Com objetos de uma classe, o operador-padrão de atribuição não torna os dois objetos o mesmo, apenas co- pia valores das variáveis-membros de um objeto para outro . Essa distinção é sutil mas importante. Talvez ajude a compreensão dos detalhes se você se lembrar de que uma variável de um tipo classe e um objeto de um tipo classe não são a mesma coisa. Um objeto é um valor de um tipo classe e pode ser armazenado em uma variável de um tipo classe, mas a variável e o objeto não são a mesma coisa. No código m3 = (m1 + m2);
a variável m3 e seu valor (m1 + m2) são coisas diferentes, exatamente como n e 5 são coisas diferentes em int n = 5;
ou em int n = (2 + 3);
Talvez leve algum tempo até que você se sinta à vontade com essa idéia de retornar valores const. Enquanto isso, uma boa regra prática é sempre retornar tipos-classes como valores const, a não ser que tenha alguma razão explícita para não o fazer. A maioria dos programas simples não será afetada por isso, a não ser pelo fato de que alguns erros sutis serão assinalados. Observe que, embora legal, é inútil retornar tipos básicos, como int, como valor const. const não tem efeito no caso de tipos básicos. Quando uma função ou operador retorna um valor de um dos tipos básicos, como int, double ou char, retorna o valor, como 5, 5.5 ou ’A’. Não retorna uma variável ou algo como uma variável. 1 Ao contrário de uma variável, o valor não pode ser alterado — não se pode alterar 5. Valores de um tipo básico não podem ser alterados quer haja um const antes do tipo retornado, quer não. Por outro lado, os valores de um tipo-classe — ou seja, objetos — podem ser alterados, já que possuem variáveis-membros, e dessa forma o modificador const exerce efeito sobre o objeto retornado. RETORNANDO VARIÁVEIS-MEMBROS DE UM TIPO-CLASSE Quando se retorna uma variável-membro de um tipo-classe, em quase todos os casos é importante retornar o valor-membro como valor const. Para ver por que, suponha que você não faça isso, como no exemplo que se segue: class Funcionario
{ public:
Dinheiro getSalario( ) { return salario; } . . . private:
Dinheiro salario; . . . };
Neste exemplo, salario é uma variável-membro privada que não deveria ser alterada a não ser por meio de uma função de acesso da classe Funcionario. Entretanto, essa privacidade é facilmente contornável, da seguinte forma: Funcionario joana; (joana.getSalario( )).entrada( );
A feliz funcionária chamada joana pode agora digitar o salário que desejar! Por outro lado, suponha que getSalario forneça seu valor como valor const, como se segue: class Funcionario
{ public: const Dinheiro getSalario( ) { return salario; }
1.
A não ser que o valor retornado seja retornado por referência, mas este é um tópico que será abordado mais adiante neste capítulo. Aqui estamos pressupondo que o valor não seja retornado por referência.
Fundamentos da Sobrecarga de Operador
215
. . . private:
Dinheiro salario; . . . }; Nesse caso, a linha seguinte provocará uma mensagem de erro do compilador. (joana.getSalario( )).entrada( ); (A declaração ideal para getSalario deveria ser const Dinheiro getSalario( ) const { return salario; } mas não queremos confundir os dois tipos de const.)
4. Suponha que você omita o const no início da declaração e definição do operador positivo sobrecarregado para a classe Dinheiro , de modo que o valor não seja retornado como valor const . O trecho seguinte é legal? Dinheiro m1(10.99), m2(23.57), m3(12.34); (m1 + m2) = m3; Se a definição da classe Dinheiro é como mostra o Painel 8.1, de modo que o operador positivo retorna seu valor como valor const, isso é legal?
SOBRECARREGANDO OPERADORES UNÁRIOS Além dos operadores binários, como + em x + y, o C++ possui operadores unários, como o operador - quando usado para significar o oposto. Um operador unário é um operador que requer apenas um operando (um argumento). Na linha abaixo, o operador unário - é utilizado para fixar o valor de uma variável x como igual ao valor negativo da variável y: ■
x = -y;
Os operadores de incremento e decremento, ++ e --, são outros exemplos de operadores unários. Podem-se sobrecarregar tanto operadores unários quanto operadores binários. Por exemplo, sobrecarregamos o operador do oposto - para o tipo Dinheiro (Painel 8.1) de modo que tenha tanto uma versão com operador unário quanto binário do operador de subtração/oposto -. Por exemplo, suponha que seu programa contenha essa definição de classe e o seguinte código: Dinheiro quantia1(10), quantia2(6), quantia3;
Então, a seguinte declaração fixa o valor de quantia3 como quantia1 menos quantia2: quantia3 = quantia1 - quantia2;
A declaração seguinte apresentará a saída R$ 4,00 na tela: quantia3.saida( );
Por outro lado, a declaração seguinte fixará quantia3 como igual ao oposto de quantia1: quantia3 = -quantia1;
A declaração seguinte apresentará na tela -R$ 10,00: quantia3.saida( );
Podem-se sobrecarregar os operadores ++ e -- de maneira similar a como sobrecarregamos o operador do oposto no Painel 8.1. Se você sobrecarregar os operadores ++ e -- seguindo o exemplo do sinal negativo - no Painel 8.1, a definição de sobrecarga se aplicará ao operador quando usado em posição de prefixo, como em ++x e --x. Mais adiante neste capítulo falaremos detalhadamente sobre a sobrecarga de ++ e -- e explicaremos como sobrecarregar esses operadores para utilizá-los na posição de sufixo.
216 ■
Sobrecarga de Operador, Amigos e Referências
SOBRECARREGANDO COMO FUNÇÕES-MEMBROS
No Painel 8.1, sobrecarregamos operadores como funções independentes definidas fora da classe. Também é possível sobrecarregar um operador como um operador-membro (função-membro). Isso é ilustrado no Painel 8.2. Observe que, quando um operador binário é sobrecarregado como um operador-membro, há apenas um parâmetro, não dois. O objeto que faz a chamada serve como o primeiro parâmetro. Por exemplo, considere o seguinte código: Dinheiro custo(1, 50), imposto(0, 15), total; total = custo + imposto;
Quando + é sobrecarregado como operador-membro, na expressão custo + imposto a variável custo é o ob jeto que faz a chamada e imposto é o argumento único de +. A definição do operador-membro + é fornecida no Painel 8.2. Observe a seguinte linha da definição: int totalCentavos1 = centavos + reais*100;
As expressões centavos e reais são variáveis-membros do objeto que faz a chamada, que, nesse caso, é o primeiro operando. Se essa definição for aplicada a custo + imposto
centavos significa custo.centavos e reais significa custo.reais.
Observe que, como o primeiro operando é o objeto que chama, deve-se, na maioria dos casos, acrescentar o modificador const ao final da declaração do operador e da definição do operador. Sempre que a invocação ao operador não alterar o objeto que faz a chamada (que é o primeiro operando), o bom estilo manda que se acrescente const ao final da declaração do operador e da definição do operador, como ilustrado no Painel 8.2. Sobrecarregar um operador como variável-membro pode parecer estranho, a princípio, mas é fácil se acostumar aos novos detalhes. Muitos especialistas aconselham a sobrecarregar os operadores sempre como operadores-membros em vez de como não-membros (como no Painel 8.1): é mais do espírito da programação orientada a objetos e um tanto mais eficiente, já que a definição pode referenciar diretamente variáveis-membros e não precisa utilizar funções de acesso e mutantes. Entretanto, como descobriremos mais adiante neste capítulo, sobrecarregar um operador como membro também apresenta uma desvantagem importante.
Painel 8.2 1 2 3 4
Sobrecarregando operadores como membros ( parte 1 de 2)
#include #include #include using namespace std;
Este é o Painel 8.1 reescrito com os operadores sobrecarregados como funções-membros.
5 //Classe para quantias de dinheiro pelo valor atual no mercado norte-americano 6 class Money 7 { 8 public: 9 Money( ); 10 Money(double amount); 11 Money(int dollars, int cents); 12 Money(int dollars); double getAmount( ) const; 13 int getDollars( ) const; 14 15 int getCents( ) const; 16 void input( ); //Lê o símbolo do dólar e a quantia. void output( ) const; 17 const Money operator +(const Money& amount2) const; 18 O objeto que faz a 19 const Money operator -(const Money& amount2) const; chamada é o primeiro bool operator ==(const Money& amount2) const; 20 operando. const Money operator -( ) const; 21 22 private: int dollars; //Uma quantia negativa é representada como dólares negativos e 23
Fundamentos da Sobrecarga de Operador Painel 8.2
24
217
Sobrecarregando operadores como membros ( parte 2 de 2)
int cents; //centavos negativos. $4.50 negativo é representado como -4 e -50.
25 int dollarsPart(double amount) const ; 26 int centsPart(double amount) const ; 27 int round(double number) const ; 28 }; 29 int main( ) 30 { 31 32 } 33 34 const Money Money::operator +(const Money& secondOperand) const 35 { 36 int allCents1 = cents + dollars*100; 37 int allCents2 = secondOperand.cents + secondOperand.dollars*100; 38 int sumAllCents = allCents1 + allCents2; 39 int absAllCents = abs(sumAllCents); //O dinheiro pode ser negativo. 40 int finalDollars = absAllCents/100; 41 int finalCents = absAllCents%100; 42 if (sumAllCents < 0) 43 { 44 finalDollars = -finalDollars; 45 finalCents = -finalCents; 46 } 47 48 }
return Money(finalDollars, finalCents);
49 const Money Money::operator -(const Money& secondOperand) const 50 51 bool Money::operator ==(const Money& secondOperand) const 52 { 53 return ((dollars == secondOperand.dollars) 54 && (cents == secondOperand.cents)); 55 } 56 const Money Money::operator -( ) const 57 { 58 return Money(-dollars, -cents); 59 } 60
UMA CLASSE TEM ACESSO A TODOS OS SEUS OBJETOS
Quando se define uma função ou operador-membro, pode-se ter acesso a qualquer variável (ou função) membro privada do objeto que faz a chamada. No entanto, você tem a permissão de fazer mais do que isso. Você tem acesso a qualquer variável-membro privada (ou função-membro privada) de qualquer objeto da classe definida. Por exemplo, considere as seguintes linhas que iniciam a definição do operador positivo para a classe Dinheiro no Painel 8.2: const Dinheiro Dinheiro:: operator +(const Dinheiro& segundoOperando) const { int totalCentavos1 = centavos + reais*100; int totalCentavos2 = segundoOperando.centavos + segundoOperando.reais*100;
218
Sobrecarga de Operador, Amigos e Referências
Nesse caso, o operador positivo é definido como um operador-membro, então as variáveis centavos e reais, na primeira linha do corpo da função, são as variáveis-membros do objeto que faz a chamada (que é exatamente o primeiro operando). Entretanto, também é legal ter acesso por nome às variáveis-membros do objeto segundoOperando, como na seguinte linha: int totalCentavos2
= segundoOperando.centavos + segundoOperando.reais*100;
Isso é legal porque segundoOperando é um objeto da classe Dinheiro e essa linha está na definição de um operador-membro da classe Dinheiro. Muitos programadores iniciantes pensam, erroneamente, que têm acesso direto aos membros privados do objeto que faz a chamada e não percebem que têm acesso direto a todos os objetos da classe definida.
5. Complete a definição do operador-membro binário — no Painel 8.2.
■
SOBRECARREGANDO A APLICAÇÃO DE FUNÇÕES ( )
O operador de chamada de função ( ) deve ser sobrecarregado como uma função-membro. Isso permite que você utilize um objeto da classe como se fosse uma função. Se a classe UmaClasse sobrecarregou o operador de aplicação de função para ter um argumento de tipo int e umObjeto é um objeto de UmaClasse, então umObjeto(42) invoca o operador de chamada de função sobrecarregado ( ) com o objeto que faz a chamada umObjeto e o argumento 42. O tipo retornado pode ser void ou qualquer outro. O operador de chamada de função ( ) é incomum, no sentido de que permite qualquer número de argumentos. Assim, você pode definir diversas versões sobrecarregadas do operador de chamada de função ( ). SOBRECARREGANDO &&, || E O OPERADOR VÍRGULA As versões predefinidas de && e || que funcionam para o tipo bool utilizam avaliação de curto-circuito. Entretanto, quando sobrecarregados esses operadores executam a avaliação completa. Isso é tão contrário ao que a maioria dos programadores espera, que causa problemas, inevitavelmente. É melhor simplesmente não sobrecarregar esses dois operadores. O operador vírgula também apresenta problemas. Em seu uso normal, o operador vírgula garante avaliações da esquerda para a direita. Quando sobrecarregado, não há garantias. O operador vírgula é outro cuja sobrecarga é melhor evitar.
8.2
Funções Amigas e Conversão de Tipo Automática Confie em seus amigos. Sabedoria popular
As funções amigas são funções não-membros que possuem todos os privilégios das funções-membros. Antes de falarmos das funções amigas em detalhes, vamos falar da conversão de tipo automática por meio de construtores, pois isso ajuda a explicar uma das vantagens de sobrecarregar operadores (ou quaisquer funções) como funções amigas. ■
CONSTRUTORES PARA CONVERSÃO DE TIPO AUTOMÁTICA
Se sua definição de classe contém os construtores apropriados, o sistema efetuará certas conversões de tipo automaticamente. Por exemplo, se o programa contiver a definição da classe Dinheiro como no Painel 8.1 ou como no Painel 8.2, você poderia utilizar o seguinte código em seu programa: Dinheiro quantiaBase(100, 60), quantiaTotal; quantiaTotal = quantiaBase + 25; quantiaTotal.saida( );
Funções Amigas e Conversão de Tipo Automática
219
A saída seria R$ 125,60
O código apresentado pode parecer simples e natural, mas há uma sutileza. O 25 (na expressão quantiaBase + 25) não é do tipo apropriado. No Painel 8.1, apenas sobrecarregamos o operador + para que ele pudesse ser utilizado com dois valores do tipo Dinheiro. Não sobrecarregamos o + para que pudesse ser utilizado com um valor do tipo Dinheiro e um inteiro. A constante 25 pode ser considerada do tipo int, mas 25 não pode ser usado como um valor do tipo Dinheiro a não ser que a definição de classe diga ao sistema, de alguma forma, como converter um inteiro em um valor do tipo Dinheiro. O sistema só sabe que 25 significa R$ 25,00 porque incluímos um construtor que requer um único argumento de tipo int. Quando o sistema vê a expressão quantiaBase + 25
verifica primeiro se o operador + foi sobrecarregado para a combinação de um valor de tipo Dinheiro e um inteiro. Como não existe tal sobrecarga, o sistema a seguir verifica se há um construtor que requer um argumento único que seja um inteiro. Se encontrar, ele o utiliza para converter o inteiro 25 em um valor de tipo Dinheiro. O construtor de argumento único diz que 25 deve ser convertido em um objeto de tipo Dinheiro cuja variávelmembro reais é igual a 25 e cuja variável-membro centavos é igual a 0. Em outras palavras, o construtor converte 25 em um objeto de tipo Dinheiro que representa R$ 25,00. (A definição do construtor está no Painel 8.1.) Observe que esse tipo de conversão não funcionará a não ser que haja um construtor adequado. Se a classe Dinheiro não contiver um construtor com um parâmetro de tipo int (ou de algum outro tipo número, como long ou double), então a expressão quantiaBase + 25
produzirá uma mensagem de erro. Essas conversões de tipo automáticas (produzidas por construtores) parecem mais comuns e atraentes com operadores numéricos sobrecarregados como + e -. Não obstante, essas conversões automáticas se aplicam exatamente da mesma forma a argumentos de funções ordinárias, argumentos de funções-membros e argumentos de outros operadores sobrecarregados. OPERADORES-MEMBROS E CONVERSÕES DE TIPO AUTOMÁTICAS Quando se sobrecarrega um operador binário como um operador-membro, os dois argumentos não são mais simétricos. Um é um objeto que faz a chamada, e apenas o segundo "argumento" é um verdadeiro argumento. Isso não só é antiestético mas também apresenta uma desvantagem bastante prática. Qualquer conversão de tipo automática só se aplicará ao segundo argumento. Assim, por exemplo, como observamos na subseção anterior, o código seguinte seria legal: Dinheiro quantiaBase(100, 60), quantiaTotal; quantiaTotal = quantiaBase + 25; Isso acontece porque Dinheiro tem um construtor com um argumento de tipo int e, assim, o valor 25 será considerado um valor int que é automaticamente convertido em um valor do tipo Dinheiro . Entretanto, se você sobrecarregar + como um operador-membro (como no Painel 8.2), não pode inverter os dois argumentos de +. A linha seguinte será ilegal quantiaTotal = 25 + quantiaBase; porque 25 não pode ser um objeto que faz uma chamada. A conversão de valores int para valores de tipo Dinheiro funciona para argumentos, mas não para objetos que fazem chamadas. Por outro lado, se você sobrecarregar + como não-membro (como no Painel 8.1), a seguinte linha será perfeitamente legal: quantiaTotal = 25 + quantiaBase; Esta é a maior vantagem de sobrecarregar um operador como um não-membro. Sobrecarregar um operador como um não-membro proporciona a você a conversão de tipo automática de todos os argumentos. Sobrecarregar um operador como membro lhe proporciona a eficiência de contornar as funções de acesso e mutantes e ter acesso direto às variáveis-membros. Há um modo de sobrecarregar um operador (e certas funções) que oferece ambas as vantagens. Chama-se sobrecarregar como uma função ami ga, e é nosso próximo tópico.
220 ■
Sobrecarga de Operador, Amigos e Referências
FUNÇÕES AMIGAS
Se sua classe tem um conjunto completo de funções de acesso e mutantes, você pode utilizar essas funções para definir operadores não-membros sobrecarregados (como no Painel 8.1, ao contrário do Painel 8.2). No entanto, embora isso possa lhe dar acesso às variáveis-membros privadas, esse acesso pode não ser eficiente. Observe novamente a definição do operador de adição sobrecarregado + dada no Painel 8.1. Em vez de apenas ler quatro variáveis-membros, ela obriga o gasto de duas invocações de getCentavos e duas de getReais. Isso aumenta a ineficiência e também pode tornar o código difícil de entender. A alternativa de sobrecarregar + como um membro contorna esse problema ao preço de perda da conversão de tipo automática do primeiro operando. Sobrecarregar o operador + como amigo permitirá que tenhamos ao mesmo tempo acesso direto às variáveis-membros e conversão de tipo automática para todos os operandos. Uma função amiga de uma classe não é uma função-membro da classe, mas tem acesso aos membros privados dessa classe (tanto às variáveis-membros privadas quanto às funções-membros privadas) exatamente como uma função-membro. Para transformar uma função em amiga, é só lhe dar o nome de amiga na definição da classe. Por exemplo, no Painel 8.3, reescrevemos a definição da classe Dinheiro mais uma vez. Desta vez sobrecarregamos os operadores como amigos. Um operador ou uma função é transformado em amigo de uma classe, listando-se a declaração do operador ou da função na definição da classe e colocando a palavra-chave friend diante da declaração do operador ou da função. Um operador amigo ou função amiga tem sua declaração listada na definição da classe, exatamente como se lista a declaração de uma função-membro, a não ser pelo fato de que se antecede a declaração com a palavra-chave friend. Entretanto, uma amiga não é uma função-membro; em vez disso, na verdade é uma função ordinária com acesso extraordinário aos membros dados da classe. A amiga é definida exatamente como a função ordinária. Em particular, as definições de operadores mostradas no Painel 8.3 não incluem o qualificador Dinheiro:: no cabeçalho da função. Além disso, não se usa a palavra-chave friend na definição da função (só na declaração da função). Os operadores amigos no Painel 8.3 são invocados da mesma forma que os operadores não-amigos, não-membros, no Painel 8.1, e possuem conversão de tipo automática de todos os argumentos exatamente como os operadores nãoamigos, não-membros, no Painel 8.1. Os tipos mais comuns de funções amigas são operadores sobrecarregados. Entretanto, qualquer tipo de função pode ser transformada em função amiga. Uma função (ou operador sobrecarregado) pode ser amigo de mais de uma classe. Para transformá-lo em um amigo de múltiplas classes, é só fornecer a declaração da função amiga em cada classe de que se deseja que seja amiga. Muitos especialistas consideram que as funções amigas (e os operadores amigos) sejam, em certo sentido, não "puras". Sentem que, no verdadeiro espírito da programação orientada a objetos, todos os operadores e funções deveriam ser funções-membros. Por outro lado, sobrecarregar operadores como amigos proporciona a vantagem pragmática da conversão de tipo automática em todos os argumentos e, como a declaração de operador fica dentro das definições de classe, proporciona pelo menos um pouco mais de encapsulação do que os operadores nãomembros, não-amigos. Apresentamos três formas de sobrecarregar operadores: como não-membros e não-amigos, como membros e como amigos. Cabe a você decidir qual técnica prefere.
Painel 8.3
Sobrecarregando operadores como amigos ( parte 1 de 2)
1 2 3 4
#include #include #include using namespace std;
5 6 7 8 9 10 11 12
//Classe para quantias de dinheiro pelo valor atual no mercado norte-americano class Money { public: Money( ); Money(double amount); Money(int dollars, int cents); Money(int dollars);
Funções Amigas e Conversão de Tipo Automática Painel 8.3
Sobrecarregando operadores como amigos ( parte 2 de 2)
13 double getAmount( ) const; 14 int getDollars( ) const ; 15 int getCents( ) const; 16 void input( ); //Lê o símbolo do dólar e a quantia. 17 void output( ) const; 18 friend const Money operator +(const Money& amount1, const Money& amount2); 19 friend const Money operator -(const Money& amount1, const Money& amount2); 20 friend bool operator ==(const Money& amount1, const Money& amount2); 21 friend const Money operator -(const Money& amount); 22 private: 23 int dollars; //Uma quantia negativa é representada como dólares negativos e 24 int cents; //centavos negativos. $4.50 negativo é representado como -4 e -50. 25 int dollarsPart(double amount) const ; 26 int centsPart(double amount) const ; 27 int round(double number) const ; 28 };
29 int main( ) 30 { 31 32 } 33 34 const Money operator +(const Money& amount1, const Money& amount2) 35 { 36 int allCents1 = amount1.cents + amount1.dollars*100; 37 int allCents2 = amount2.cents + amount2.dollars*100; 38 int sumAllCents = allCents1 + allCents2; 39 int absAllCents = abs(sumAllCents); //O dinheiro pode ser negativo 40 int finalDollars = absAllCents/100; 41 int finalCents = absAllCents%100; Observe que as funções amigas têm acesso direto às variáveis-membros. 42 if (sumAllCents < 0) 43 44 45 46
{
47 48 }
return Money(finalDollars, finalCents);
finalDollars = -finalDollars; finalCents = -finalCents; }
49 const Money operator -(const Money& amount1, const Money& amount2) 50 51 bool operator ==(const Money& amount1, const Money& amount2) 52 { 53 return ((amount1.dollars == amount2. dollars) 54 && (amount1.cents == amount2. cents )); 55 } 56 const Money operator -(const Money& amount) 57 { 58 return Money(-amount.dollars , -amount. cents ); 59 } 60
221
222
Sobrecarga de Operador, Amigos e Referências
FUNÇÕES AMIGAS Uma função amiga de uma classe é uma função ordinária, a não ser pelo fato de que tem acesso aos membros privados de ob jetos dessa classe. Para tornar uma função amiga de uma classe , você deve listar a declaração de função da função amiga na definição de classe. A declaração de função é precedida pela palavra-chave friend. A declaração de função pode ser colocada na seção privada ou na seção pública, mas será uma função pública em ambos os casos; portanto, o programa fica mais claro se você a listar na seção pública.
SINTAXE DE UMA DEFINIÇÃO DE CLASSE COM FUNÇÕES AMIGAS class Nome_da_Classe
{ public: friend Declaracao_para_Funcao_Amiga_1
Não é preciso listar as funções amigas primeiro. Pode-se misturar a ordem das declarações.
friend Declaracao_para_Funcao_Amiga_2
. . . Declaracoes_da_Funcao_Membro private : Declaracoes_Membro_Privadas };
EXEMPLO class TanqueCheio
{ public: friend void encheInferior(TanqueCheio& t1, TanqueCheio& t2);
//Enche o tanque até o nível inferior de combustível, ou t1 se os dois forem iguais. TanqueCheio(double aCapacidade, double oNivel); TanqueCheio( ); void input( ); void output( ) const; private : double capacidade; //em litros double nivel; }; Uma função amiga não é uma função-membro. Uma função amiga é definida e chamada da mesma forma que uma função ordinária. Não se utiliza o operador ponto em uma chamada a uma função amiga, e não se utiliza um qualificador de tipo na definição de uma função amiga.
COMPILADORES SEM AMIGAS Em alguns compiladores de C++, as funções amigas simplesmente não funcionam como deveriam. Pior ainda, podem funcionar algumas vezes e outras não. Nesses compiladores, as funções amigas nem sempre têm acesso a membros privados da classe, como deveriam ter. Presumivelmente, isso será consertado em versões posteriores desses compiladores. Enquanto isso, você terá de contornar esse problema. Se você tiver um desses compiladores em que as funções amigas não funcionam, deve utilizar funções de acesso para definir funções não-membros e operadores sobrecarregados ou sobrecarregar operadores como membros.
■ CLASSES AMIGAS
Uma classe pode ser amiga de outra classe da mesma forma que uma função pode ser amiga de uma classe. Se a classe F é amiga da classe C, então toda função-membro da classe F é uma amiga da classe C. Para transformar uma classe em amiga de outra, você deve declarar a classe amiga como amiga dentro da outra classe. Quando uma classe é amiga de outra classe, geralmente há uma referência de uma classe à outra em suas definições de classe. Isso requer que você inclua uma declaração antecipada para a classe definida em segundo lugar, como ilustrado no esboço que segue este parágrafo. Observe que a declaração antecipada é apenas o cabeçalho da definição de classe seguido de um ponto-e-vírgula.
Referências e Mais Operadores Sobrecarregados
223
Se você quer que a classe F seja amiga da classe C, deve escrever algo desse tipo: class F; //declaração antecipada class C
{ public:
... friend class F;
... }; class F
{ ...
Exemplos completos utilizando classes amigas são fornecidos no Capítulo 17. Não utilizaremos classes amigas até então.
6. Qual é a diferença entre uma função amiga de uma classe e uma função-membro de uma classe? 7. Complete a definição do operador amigo de subtração — no Painel 8.3. 8. Suponha que você deseje sobrecarregar o operador < para aplicá-lo ao tipo Dinheiro definido no Painel 8.3. O que você precisa acrescentar à definição de Dinheiro fornecida nesse painel?
8.3
Referências e Mais Operadores Sobrecarregados Não confunda a lua com o dedo que a aponta. Ditado Zen
Esta seção trata de assuntos especializados, mas importantes, a respeito da sobrecarga, inclusive a sobrecarga do operador de atribuição e dos operadores <<, >>, [ ], ++ e --. Como é necessário entender o fornecimento de uma referência para sobrecarregar corretamente alguns desses operadores, também tratamos desse tópico. REGRAS A RESPEITO DA SOBRECARGA DE OPERADORES ■ ■ ■ ■ ■
■
■ ■
Quando se sobrecarrega um operador, pelo menos um parâmetro (um operando) do operador sobrecarregado resultante deve ser de um tipo classe. A maioria dos operadores pode ser sobrecarregada como um membro da classe, ou um não-membro, não-amigo. Os seguintes operadores só podem ser sobrecarregados como membros (não-estáticos) da classe: =, [ ], -> e ( ). Não se pode criar um novo operador. Tudo o que se pode fazer é sobrecarregar operadores existentes, como +, -, *, /, %, etc. Não se pode alterar o número de argumentos que um operador requer. Por exemplo, não se pode mudar % de um operador binário para um unário quando se sobrecarrega %; não se pode mudar ++ de um operador unário para um binário ao sobrecarregá-lo. Não se pode alterar a precedência de um operador. Um operador sobrecarregado tem a mesma precedência que a versão ordinária do operador. Por exemplo, x*y + z sempre significa (x*y) + z, mesmo que x, y e z sejam objetos e os operadores + e * tenham sido sobrecarregados para as classes adequadas. Os seguintes operadores não podem ser sobrecarregados: o operador ponto (.), o operador de resolução de escopo (::), sizeof, ?: e o operador .*, que não será discutido neste livro. Um operador sobrecarregado não pode ter argumentos-padrão.
224
Sobrecarga de Operador, Amigos e Referências
REFERÊNCIAS Uma referência é o nome de uma posição de armazenamento.2 Pode-se ter uma referência independente, como no seguinte exemplo: ■
int roberto; int&
beto = roberto;
Isso torna beto uma referência à posição de armazenamento da variável roberto, que transforma beto em um nome alternativo, apelido (alias ) da variável roberto. Qualquer alteração em beto também será feita em roberto. Dito dessa forma, parece que uma referência independente não passa de uma forma de tornar seu código confuso e colocar você em encrenca. Na maioria das vezes, uma referência independente só causa confusão, embora existam alguns poucos casos em que pode ser útil. Não falaremos mais em referências independentes nem as utilizaremos. Como você deve desconfiar, as referências são utilizadas para implementar o mecanismo de parâmetros chamados por referência. Assim, o conceito não é totalmente novo para você, embora o termo referência seja. Estamos interessados em referências porque retornar uma referência permitirá que se sobrecarreguem certos operadores de um modo mais natural. Retornar uma referência pode ser encarado como algo como retornar uma variável ou, mais precisamente, um nome alternativo para uma variável. Os detalhes sintáticos são simples. Acrescenta-se um & ao tipo retornado. Por exemplo: double&
amostraFuncao( double& variavel);
Já que um tipo como double& é um tipo diferente de double, você deve usar o & tanto na declaração da função quanto na definição. A expressão fornecida deve ser algo com uma referência, como uma variável do tipo apropriado. Não pode ser uma expressão, como X + 5. Embora muitos compiladores permitam que você o faça (com resultados infelizes), você também não deve retornar uma variável local, porque estaria gerando um nome alternativo para uma variável e imediatamente a destruindo. Um exemplo trivial da definição de função é double&
amostraFuncao( double& variavel)
{ return variavel;
}
Claro que esta é uma função bastante inútil, até mesmo perigosa, mas ilustra a idéia. Por exemplo, o código seguinte apresentará como saída 99 e depois 42: double m
= 99; cout << amostraFuncao(m) << endl; amostraFuncao(m) = 42; cout << m << endl;
Só estaremos retornando uma referência quando definirmos certos tipos de operadores sobrecarregados. L-VALUES E R-VALUES O termo l-value é empregado para algo que pode aparecer ao lado esquerdo de um operador de atribuição. O termo empregado para algo que pode aparecer ao lado direito de um operador de atribuição. Se você quiser que o objeto retornado por uma função seja um l-value, ele deve ser retornado por referência.
r-value
é
RETORNANDO UMA REFERÊNCIA A CERTAS VARIÁVEIS-MEMBROS Quando uma função-membro retorna uma variável-membro e essa variável-membro é de algum tipo-classe, normalmente ela não deveria ser fornecida por referência. Por exemplo, considere class A
2.
Se você conhece ponteiros, notará que a referência se parece com um ponteiro. Uma referência é, em essência, mas não exatamente, um ponteiro constante. Existem diferenças entre ponteiros e referências, e os dois não são completamente intercambiáveis.
Referências e Mais Operadores Sobrecarregados
225
{ public:
const AlgumaClasse getMembro( ) { return membro; } ... private:
AlgumaClasse membro; ... };
em que AlgumaClasse é, obviamente, um tipo classe. A função getMembro não deve retornar uma referência, mas sim retornar um valor const, como fizemos no exemplo. O problema de retornar uma referência a uma variável-membro de tipo classe é o mesmo que descrevemos quanto a retornar a variável-membro como valor não- const na seção "Dica" deste capítulo intitulada Retornando Variáveis-Membros de um Tipo-Classe. Quando se retorna uma variável-membro que é ela mesma de um tipo-classe, normalmente ela deve ser fornecida como valor const. (Cada uma dessas regras possui raras exceções.)
■ SOBRECARREGANDO >> E <<
Os operadores >> e << podem ser sobrecarregados de modo que sejam usados para a entrada e saída de objetos das classes que você define. Os detalhes não são muito diferentes dos que já vimos para outros operadores, mas há algumas novas sutilezas. O operador de inserção << que utilizamos com cout é um operador binário bem semelhante a + ou -. Por exemplo, considere o exemplo: cout << "Ei, você aí.\n";
O operador é <<, o primeiro operando é o objeto predefinido cout (da biblioteca iostream), e o segundo operando é o valor string "Ei, você aí.\n". O objeto predefinido cout é do tipo ostream e, portanto, você pode sobrecarregar <<, o parâmetro que recebe cout será do tipo ostream. Pode-se alterar qualquer um dos dois operandos para <<. Quando estudarmos a E/S de arquivos no Capítulo 12, você verá como criar um objeto de tipo ostream que envie saída para um arquivo. (Esses objetos de E/S de arquivos, assim como os objetos cin e cout são chamados de streams , e é por isso que o nome da biblioteca é ostream.) A sobrecarga que criamos, tendo cout em mente, funcionará também para a saída de arquivos sem qualquer alteração na definição do sobrecarregado <<. Em nossas definições anteriores da classe Dinheiro (do Painel 8.1 ao 8.3), utilizamos a função-membro saida para enviar à saída valores do tipo Dinheiro. Isso é adequado, mas seria melhor se pudéssemos simplesmente utilizar o operador de inserção << para enviar à saída valores do tipo Dinheiro, como no exemplo: Dinheiro quantia(100); cout << "Eu tenho " << quantia << " em minha carteira.\n";
em vez de ter de utilizar a função-membro saida, como mostrado a seguir: Dinheiro quantia(100); cout << "Eu tenho "; quantia.saida( ); cout << " em minha carteira.\n";
Um problema de se sobrecarregar o operador << é decidir qual valor deve ser retornado, se é que algum deve sê-lo, quando << é utilizado em uma expressão como a seguinte: cout << quantia
Os dois operandos na expressão acima são cout e quantia, e avaliar a expressão fará com que o valor de quantia seja escrito na tela. Mas, se << é um operador como + ou -, a expressão acima também deveria retornar algum valor. Afinal, expressões com outros operandos, como n1 + n2, retornam valores. Mas qual quantia cout<< retorna? Para obter a resposta a esta questão, precisamos olhar para uma expressão mais complexa, envolvendo <<. Consideremos a seguinte expressão, que envolve a avaliação de cadeias de expressões utilizando <<:
226
Sobrecarga de Operador, Amigos e Referências
cout << "Eu tenho " << quantia << " em minha carteira.\n";
Se você acha que o operador << é análogo a outros operadores, como +, então a linha acima deveria ser (e de fato é) equivalente à seguinte: ( (cout << "Eu tenho ") << quantia ) << " em minha carteira.\n";
Que valor << deveria retornar para que a expressão acima fizesse sentido? A primeira parte avaliada é a subexpressão: (cout << "Eu tenho ")
Se tudo estiver funcionando, a subexpressão acima deve retornar cout, para que o cálculo possa continuar: ( cout << quantia ) << " em minha carteira.\n";
E se tudo continuar funcionando, (cout << quantia) também deve retornar cout para que o cálculo possa continuar: cout << " em minha carteira.\n";
Isso é ilustrado no Painel 8.4. O operador << deve retornar seu primeiro argumento, que é do tipo ostream (o tipo de cout). Assim, a declaração para o operador sobrecarregado << (para utilizar com a classe Dinheiro) deveria ser: class Dinheiro { public: . . . friend ostream& operator <<(ostream& outs, const Dinheiro& quantia); Painel 8.4
<< como Operador
cout << "Tenho " << amount << " em minha bolsa.\n"; significa o mesmo que ((cout << "Tenho ") << amount) << " em minha bolsa.\n"; e é calculado na seguinte forma: Primeiro se calcula (cout << "Tenho "), que retorna cout: ((cout << "Tenho ") << amount) << " em minha bolsa.\n"; A string " Tenho" é apresentada à saída.
(cout << amount) << " em minha bolsa.\n";
Então se calcula (cout << amount), que retorna cout: (cout << amount) << " em minha bolsa.\n"; O valor de
amount é
apresentado à saída.
cout << " em minha bolsa.\n"; Então se calcula cout << " em minha bolsa.\n", que retorna cout: cout << " em minha bolsa.\n"; A string "em
cout;
minha
bolsa.\n"
é apresentada a
Como não há operadores <<, o processo termina.
Referências e Mais Operadores Sobrecarregados
227
Uma vez que houvermos sobrecarregado o operador de inserção (saída) <<, não precisaremos mais da funçãomembro saida e apagaremos saida de nossa definição da classe Dinheiro. A definição do operador sobrecarregado << é bastante similar à função-membro saida. Eis um esboço da definição para o operador sobrecarregado: ostream& operator <<(ostream& saidaStream, const Dinheiro& quantia) { return saidaStream;
}
Observe que o operador retorna uma referência. O operador de extração >> é sobrecarregado de forma análoga à que descrevemos para o operador de inserção <<. Entretanto, com o operador de extração (entrada) >>, o segundo argumento será o objeto que recebe o valor de entrada, então o segundo parâmetro deve ser um parâmetro comum chamado por referência. Eis um esboço da definição para o operador de extração sobrecarregado >>: istream& operator >>(istream& entradaStream, Dinheiro& quantia) { return entradaStream;
}
As definições completas dos operadores sobrecarregados << e >> são dadas no Painel 8.5, em que reescrevemos a classe Dinheiro mais uma vez. Dessa vez, reescrevemos a classe para que os operadores << e >> fossem sobrecarregados para permitir seu uso com valores de tipo Dinheiro. Observe que não se pode realmente sobrecarregar >> ou << como operadores-membros. Se << e >> devem funcionar como queremos, o primeiro operando (primeiro argumento) deve ser cout ou cin (ou algum stream de arquivo de E/S). Mas se queremos sobrecarregar os operadores como membros, digamos, da classe Dinheiro, então o primeiro operando terá de ser o objeto que faz a chamada e, assim, terá de ser de tipo Dinheiro, e isso não permitirá a definição dos operadores de modo que se comportem normalmente para >> e <<. Painel 8.5
1 2 3 4
Sobrecarregando << e >> ( parte 1 de 3)
#include #include #include using namespace std;
5 //Classe para quantias de dinheiro pelo valor atual no mercado norte-americano 6 class Money 7 { 8 public: 9 Money( ); 10 Money(double amount); 11 Money(int theDollars, int theCents); 12 Money(int theDollars); 13 double getAmount( ) const; 14 int getDollars( ) const ; 15 int getCents( ) const; 16 friend const Money operator +(const Money& amount1, const Money& amount2);
228
Sobrecarga de Operador, Amigos e Referências
Painel 8.5
17 18 19 20 21 22 23 24
Sobrecarregando << e >> ( parte 2 de 3)
friend const Money operator -(const Money& amount1, const Money& amount2); friend bool operator ==(const Money& amount1, const Money& amount2); friend const Money operator -(const Money& amount); friend ostream& operator <<(ostream& outputStream, const Money& amount); friend istream& operator >>(istream& inputStream, Money& amount); private : int dollars; //Uma quantia negativa é representada como dólares negativos e int cents; //centavos negativos. $4.50 é representado como -4 e -50.
25 int dollarsPart(double amount) const ; 26 int centsPart(double amount) const ; 27 int round(double number) const ; 28 }; 29 int main( ) 30 { 31 Money yourAmount, myAmount(10, 9); 32 cout << "Digite uma quantia de dinheiro: "; 33 cin >> yourAmount; 34 cout << "A sua quantia é " << yourAmount << endl; 35 cout << "Minha quantia é " << myAmount << endl; 36 37 if (yourAmount == myAmount) 38 cout << "Nós temos a mesma quantia.\n"; 39 else 40 cout << "Um de nós é mais rico.\n"; 41 42 43
Money ourAmount = yourAmount + myAmount; cout << yourAmount << " + " << myAmount << " igual a " << ourAmount << endl;
44 45 46
Money diffAmount = yourAmount - myAmount; cout << yourAmount << " - " << myAmount << " igual a " << diffAmount << endl;
Como << fornece uma referência, pode-se encadear <<. Dessa forma, >> pode ser encadeado de maneira semelhante.
47 return 0; 48 }
49 ostream& operator <<(ostream& outputStream, const Money& amount) 50 { 51 int absDollars = abs(amount.dollars); Na função main, cout é 52 int absCents = abs(amount.cents); conectada a output Stream. 53 if (amount.dollars < 0 || amount.cents < 0) 54 //trata do caso em que dólares == 0 ou centavos == Se0 desejar um outro algoritmo de entrada, veja 55 outputStream << "$-"; o Exercício de Autoteste 3 no Capítulo 7. 56 else 57 outputStream << ’$’; 58 outputStream << absDollars; 59 60 61 62
if (absCents >= 10)
63 64 }
return outputStream;
outputStream << ’.’ << absCents; else
outputStream << ’.’ << ’0’ << absCents; Fornece uma referência.
Referências e Mais Operadores Sobrecarregados Painel 8.5
229
Sobrecarregando << e >> ( parte 3 de 3)
65 66 //Utiliza iostream e cstdlib: 67 istream& operator >>( istream& inputStream, Money& amount) 68 { 69 char dollarSign; 70 inputStream >> dollarSign; //esperamos que sim Na função main, cin é conectado a 71 if (dollarSign != ’$’) inputStream . 72 { 73 cout << "Não há símbolo de dólar na entrada Money.\n"; 74 exit(1); Como este não é um operador-membro, é 75 } preciso especificar um objeto de chamada para as funções-membros de Money. 76 double amountAsDouble; 77 78 79
inputStream >> amountAsDouble; amount.dollars = amount.dollarsPart(amountAsDouble); amount.cents = amount.centsPart(amountAsDouble);
80 81 }
return inputStream;
Fornece uma referência.
DIÁLOGO PROGRAMA-USUÁRIO Enter an amount of money: $123.45 Your amount is $123.45 My amount is $10.09. One of us is richer. $123.45 + $10.09 equals $133.54 $123.45 - $10.09 equals $113.36
9. No Painel 8.5, a definição do operador sobrecarregado << contém linhas como a seguinte: saidaStream << "R$-"; Isso não é circular? Não estamos definindo << em termos de < 10. Por que não podemos sobrecarregar << ou >> como operadores-membros? 11. Apresentamos, a seguir, a definição de uma classe chamada Porcentagem. Objetos do tipo Porcentagem representam porcentagens como 10% ou 99%. Dê as definições dos operadores sobrecarregados >> e << para que possam ser utilizados para entrada e saída com objetos da classe Porcentagem. Presuma que a entrada sempre consiste em um inteiro seguido pelo caractere ’%’, como em 25%. Todas as porcentagens são números inteiros e são armazenadas na variável-membro int chamada valor. Você não precisa definir os outros operadores sobrecarregados nem o construtor. Defina apenas os operadores sobrecarregados >> e <<. #include using namespace std; class Porcentagem
{ public: friend bool operator ==(const Porcentagem& primeiro, const Porcentagem& segundo); friend bool operator <(const Porcentagem& primeiro, const Porcentagem& segundo);
Porcentagem( ); friend istream& operator >>(istream& entradaStream, Porcentagem& umaPorcentagem); friend ostream& operator <<(ostream& saidaStream,
230
Sobrecarga de Operador, Amigos e Referências
const Porcentagem& umaPorcentagem); //Normalmente haveria outros membros e amigos. private: int valor;
};
SOBRECARREGANDO >> E << Os operadores de entrada e saída >> e << podem ser sobrecarregados como outros operadores. Se você quer que os operadores se comportem como esperado com cin, cout e arquivos de E/S, então o valor retornado deve ser de tipo istream para a entrada e ostream para a saída, e o valor deve ser retornado por referência.
DECLARAÇÕES class Nome_Da_Classe
{ . . . public:
. . . friend istream& operator >>(istream& Parametro_1,
Nome_Da_Classe& Parametro_2); friend ostream& operator <<(ostream& Parametro_3, const Nome_Da_Classe& Parametro_4);
. . . Os operadores não precisam ser amigos, mas não podem ser membros da classe-alvo de entrada ou saída.
DEFINIÇÕES istream& operator >>(istream& Parametro_1, Nome_Da_Classe& Parametro_2) { . . . } ostream& operator <<(ostream& Parametro_3, const Nome_Da_Classe& Parametro_4) { . . . } Se você possui suficientes funções de acesso e mutantes, pode sobrecarregar >> e << como funções não-amigas. Entretanto, é natural e mais eficiente defini-las como amigas.
QUE MODO DE VALOR RETORNADO UTILIZAR Uma função pode retornar um valor de tipo T em quatro formas diferentes: Por valor simples, como na declaração de função T f( ); Por valor constante, como na declaração de função const T f( ); Por referência, como na declaração de função T& f( ); Por referência const, como na declaração de função const T& f( ); Não existe consenso sobre quando utilizar cada uma delas. Assim, não espere muita consistência em seu uso. Mesmo quando um autor ou programador tem uma política clara, raramente consegue segui-la sem exceções. Ainda assim, alguns pontos são claros. Se você vai retornar um tipo simples, como int ou char, não há razão para utilizar um const quando se retorna por valor ou por referência. Assim, os programadores em geral não usam um const no tipo retornado quando é um tipo simples. Se você quer que o valor simples retornado possa ser um l-value, ou seja, possa constar no lado esquerdo de uma declaração de atribuição, forneça por referência; de outra forma, forneça o tipo simples por valor simples. Tipos-classe não são tão simples. O restante desta discussão se aplica ao fornecimento de um objeto de um tipo-classe.
Referências e Mais Operadores Sobrecarregados
231
A decisão de se retornar ou não por referência tem a ver com sua vontade ou não de poder utilizar o objeto retornado como um l-value. Se você quer que o valor simples retornado possa ser um l-value, ou seja, possa ser utilizado no lado esquerdo de um operador de atribuição, você deve retornar por referência e, assim, deve utilizar um caractere de "e" comercial, &, junto ao tipo retornado. O fornecimento de uma variável local (ou outro objeto de vida curta) por referência, com ou sem um const, pode causar problemas e deve ser evitado. Para tipos-classe, as duas especificações de tipo retornado const T e const T& são bastante similares. Ambas significam que não se pode alterar o objeto retornado invocando alguma função mutante diretamente sobre o objeto retornado, como em f( ).mutante( );
O valor retornado ainda pode ser copiado para outra variável com um operador de atribuição e essa outra variável pode receber a aplicação de uma função mutante. Se você estiver em dúvida entre const T& e const T, utilize const T (sem o "e" comercial). Um const T& talvez seja um pouco mais eficiente que um const T.3 Todavia, a diferença normalmente não é tão importante e a maioria dos programadores utiliza const T em vez de const T& como especificação para o tipo retornado. Como já observado, const T& às vezes causa problemas. O resumo seguinte pode ser útil. Presume-se que T seja de tipo-classe. Só falaremos de construtores de cópia no Capítulo 10, mas incluímos detalhes a respeito deles como referência. Se você ainda não leu o Capítulo 10, ignore todas as referências a construtores de cópia. Se uma função-membro pública retorna uma variável-membro de classe privada, deve sempre ter um const junto ao tipo retornado, como explicamos na seção "Armadilha" deste capítulo, intitulada Retornando Variáveis-Membros de um Tipo Classe. (Uma exceção a esta regra é que os programadores normalmente retornam um valor de tipo string por valor ordinário, não por valor const. Isso talvez porque o tipo string é considerado semelhante a um tipo simples como int e char, embora string seja um tipo classe.) O resumo seguinte pode ser útil. Presume-se que T seja de tipo-classe. Fornecimento simples por valor, como na declaração de função T f( ); Não pode ser utilizado como l-value e o valor retornado pode ser alterado diretamente, como em f( ).mutante( ). Chama o construtor de cópia.
Fornecimento por valor constante, como em const T f( ); Este caso é como o anterior, mas o valor retornado não pode ser alterado diretamente como em f( ).mutante( ). Fornecimento por referência como em T& f( ); Pode ser utilizado como um l-value, e o valor retornado pode ser alterado diretamente como em f( ).mutante( ). Não chama o construtor de cópia. Fornecimento por referência constante, como em const T& f( ); Não pode ser utilizado como um l-value, e o valor retornado não pode ser alterado diretamente como em f( ).mutante( ). Não chama o construtor de cópia. 3
■
OPERADOR DE ATRIBUIÇÃO
Se você sobrecarregar o operador de atribuição =, deve sobrecarregá-lo como um operador-membro. Se você não sobrecarregar o operador de atribuição =, receberá automaticamente um operador de atribuição para sua classe. Esse operador de atribuição padrão copia os valores de variáveis-membros de um objeto da classe para as variáveis-membros correspondentes de outro objeto da classe. Para classes simples, normalmente é isso o que você quer. Quando tratarmos de ponteiros, esse operador de atribuição padrão não será o que queremos; quando chegarmos lá, falaremos em sobrecarregar o operador de atribuição. ■
SOBRECARREGANDO OS OPERADORES DE INCREMENTO E DECREMENTO
Os operadores de incremento e decremento ++ e -- possuem, cada um, duas versões. Podem fazer coisas diferentes, dependendo da utilização na notação de prefixo, ++x, ou sufixo, x++. Assim, quando sobrecarregamos esses operadores, precisamos, de algum modo, distinguir entre as versões de prefixo e sufixo para que tenhamos duas versões do operador sobrecarregado. Em C++, essa distinção entre as versões em prefixo e sufixo é tratada de for3.
Isto porque const T& não chama o construtor de cópia, enquanto const T sim. Trataremos dos construtores de cópia no Capítulo 10.
232
Sobrecarga de Operador, Amigos e Referências
ma que, à primeira leitura (e talvez até à segunda), pareça um tanto ardilosa. Se você sobrecarregar o operador ++ da forma normal (como operador não-membro com um parâmetro ou como operador-membro sem parâmetros), você sobrecarregou a forma prefixada. Para obter a versão sufixada, x++ ou x--, acrescente um segundo parâmetro de tipo int. É apenas um marcador para seu compilador; não se fornece um segundo argumento int quando se invoca x++ ou x--. Por exemplo, o Painel 8.6 contém a definição de uma classe cujos dados são pares de inteiros. O operador de incremento ++ é definido de modo que funcione tanto na notação de prefixo quanto na de sufixo. Definimos ++ de modo que aja intuitivamente como ++ em variáveis int. Esta é a melhor forma de definir ++, mas você é livre defini-lo a fim de retornar qualquer tipo e executar qualquer ação. A definição da versão em sufixo ignora esse parâmetro int, como mostrado no Painel 8.6. Quando o compilador vê a++, trata como uma invocação a IntPar::operator++(int), com a como o objeto que faz a chamada. O operador de incremento e decremento em tipos simples, como int e char, retorna por referência na forma em prefixo e por valor na forma em sufixo. Se quiser reproduzir o que acontece com tipos simples quando se sobrecarregam esses operadores para seus tipos-classe, você retornará por referência para a forma em prefixo e por valor para a forma em sufixo. Entretanto, descobrimos que retornar por referência com operadores de incremento ou decremento abre a porta para inúmeros problemas e, por isso, sempre retornamos por valor para todas as versões dos operadores de incremento e decremento.
12. O trecho seguinte é correto? Explique sua resposta. (A definição de IntPar é fornecida no Painel 8.6.) IntPar a(1,2); (a++)++;
■
SOBRECARREGANDO O OPERADOR VETOR [ ]
Pode-se sobrecarregar os colchetes, [ ], para uma classe de modo que possam ser utilizados com objetos da classe. Se você quer utilizar [ ] em uma expressão no lado esquerdo de um operador de atribuição, o operador deve ser definido para retornar uma referência. Quando se sobrecarrega [ ], o operador [ ] deve ser uma função-membro. Painel 8.6 Sobrecarregando ++ ( parte 1 de 3) 1 #include 2 #include 3 using namespace std; 4 class IntPair 5 { 6 public: 7 IntPair(int firstValue, int secondValue); 8 IntPair operator++( ); //versão com prefixo 9 IntPair operator++(int); //versão com sufixo 10 void setFirst(int newValue); void setSecond(int newValue); 11 int getFirst( ) const; 12 13 int getSecond( ) const; 14 private: int first; 15 int second; 16 17 }; 18 int main( ) 19 { 20 IntPair a(1,2); 21 cout << "Sufixo a++: Valor inicial do objeto a: "; 22 cout << a.getFirst( ) << " " << a.getSecond( ) << endl; 23 IntPair b = a++;
Não é preciso dar um nome de parâmetro em uma declaração de função ou de operador. Para ++ é interessante não dar parâmetros, já que o parâmetro não é utilizado.
Referências e Mais Operadores Sobrecarregados Painel 8.6
Sobrecarregando ++ ( parte 2 de 3)
24 25 26 27
cout cout cout cout
<< << << <<
"Valor retornado: "; b.getFirst( ) << " " << b.getSecond( ) << endl; "Objeti alterado: "; a.getFirst( ) << " " << a.getSecond( ) << endl;
28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
a = IntPair(1, 2); cout << "Prefixo a++: Valor inicial do objeto a: "; cout << a.getFirst( ) << " " << a.getSecond( ) << endl; IntPair c = ++a; cout << "Valor retornado: "; cout << c.getFirst( ) << " " << c.getSecond( ) << endl; cout << "Objeto alterado: "; cout << a.getFirst( ) << " " << a.getSecond( ) << endl; return 0; } IntPair::IntPair(int firstValue, int secondValue) : first(firstValue), second(secondValue) {/*Corpo propositadamente vazio*/} IntPair IntPair::operator++(int ignoreMe) //Versão com sufixo { int temp1 = first; int temp2 = second; first++; second++; return IntPair(temp1, temp2); }
50 IntPair IntPair::operator++( ) //Versão com prefixo 51 { 52 first++; 53 second++; 54 return IntPair(first, second); 55 } 56 void IntPair::setFirst(int newValue) 57 { 58 first = newValue; 59 } 60 void IntPair::setSecond(int newValue) 61 { 62 second = newValue; 63 } 64 int IntPair::getFirst( ) const 65 { return first; 66 67 } 68 int IntPair::getSecond( ) const 69 { 70 return second; 71 }
DIÁLOGO PROGRAMA-USUÁRIO Sufixo a++: Valor inicial do objeto a: 1 2 Valor retornado: 1 2 Objeto alterado: 2 3
233
234
Sobrecarga de Operador, Amigos e Referências
Painel 8.6
Sobrecarregando ++ ( parte 3 de 3)
Prefixo a++: Valor inicial do objeto a: 1 2 Valor retornado: 2 3 Objeto alterado: 2 3
É interessante rever a sintaxe para o operador [ ], já que é diferente de todos os outros operadores que vimos. Lembre-se de que [ ] é sobrecarregado como um operador-membro; portanto, o componente da expressão que utilize [ ] deve ser o objeto que faz a chamada. Na expressão a[2], a é o objeto que faz a chamada e 2 é o argumento do operador-membro [ ]. Quando se sobrecarrega [ ], este parâmetro "índice" pode ser de qualquer tipo. Por exemplo, no Painel 8.7 definimos uma classe chamada Par, cujos objetos se comportam como vetores de caracteres com os dois índices 1 e 2 (não 0 e 1). Observe que as expressões a[1] e a[2] se comportam exatamente como variáveis indexadas de vetor. Se você observa a definição do operador sobrecarregado [ ], verá que uma referência é fornecida e que é uma referência a uma variável-membro, não ao objeto Par inteiro. Isso porque a variávelmembro é análoga a uma variável indexada de um vetor. Quando se altera a[1] (no código-exemplo no Painel 8.7), deseja-se que esta seja uma alteração na variável-membro primeiro. Observe que isso dá a qualquer programa acesso às variáveis-membros privadas, por exemplo, via a[1] e a[2] na função main do exemplo no Painel 8.7. Embora primeiro e segundo sejam membros privados, o código é legal porque não referencia primeiro e segundo por nome, e sim indiretamente, utilizando os nomes a[1] e a[2]. Painel 8.7
1 2 3
Sobrecarregando [ ] ( parte 1 de 2)
#include #include using namespace std;
4 class CharPair 5 { 6 public: 7 CharPair( ){/*Corpo propositadamente vazio*/} 8 CharPair(char firstValue, char secondValue) 9 : first(firstValue), second(secondValue) 10 {/*Corpo propositadamente vazio*/} 11 char& operator[](int index); 12 13 private: 14 char first; 15 char second; 16 };
17 int main( ) 18 { 19 CharPair a; 20 a[1] = ’A’; 21 a[2] = ’B’; 22 cout << "a[1] e a[2] são:\n"; 23 cout << a[1] << a[2] << endl; 24 25 26 27 28 29 } 30
cout << "Digite duas letras (sem espaços):\n"; cin >> a[1] >> a[2]; cout << "Você digitou:\n"; cout << a[1] << a[2] << endl; return 0;
Referências e Mais Operadores Sobrecarregados Painel 8.7
31 32 33 34 35 36 37 38 39 40 41 42 43
235
Sobrecarregando [ ] ( parte 2 de 2)
//Utiliza iostream e cstdlib: char & CharPair::operator[](int index) { if (index == 1) return first; else if (index == 2) return second;
Observe que o que é fornecido é a variável-membro, não o objeto entire Pair, porque a variável-membro é análoga a uma variável indexada de um vetor.
else
{ cout << "Valor de índice ilegal.\n"; exit(1); } }
DIÁLOGO PROGRAMA-USUÁRIO a[1] e a[2] são: AB Digite duas letras (sem espaços): CD Você digitou: CD
■ SOBRECARGA COM BASE EM L-VALUE VERSUS R-VALUE
Não faremos isto neste livro, mas você pode sobrecarregar um nome de função (ou operador) para que se comporte de modo diferente quando utilizado como l-value e quando utilizado como r-value. (Lembre-se de que l-value é o que pode ser utilizado no lado esquerdo de uma declaração de atribuição.) Por exemplo, se você quer que uma função f se comporte de modo diferente dependendo de sua utilização como um l-value ou um r-value, pode fazer o seguinte: class AlgumaClasse
{ public: int& f( ); // será usado em qualquer invocação a l-value const int& f( ) const; // usado em qualquer invocação a r-value
};
...
As duas listas de parâmetros não precisam ser vazias, mas devem ser iguais (senão você obtém a sobrecarga simples). Não deixe de notar que a segunda declaração de f apresenta duas ocorrências de const. Você deve incluir ambas as ocorrências. O sinal de "e" comercial, &, também é exigido, é claro.
■
■ ■
■
Operadores, como + e ==, podem ser sobrecarregados para ser utilizados com objetos de um tipo-classe que você define. Um operador é apenas uma função que utiliza uma sintaxe diferente para as invocações. Uma função amiga de uma classe é uma função ordinária, a não ser pelo fato de ter acesso aos membros privados da classe, exatamente como as funções-membros. Quando um operador é sobrecarregado como membro de uma classe, o primeiro operando é o objeto que faz a chamada.
236
Sobrecarga de Operador, Amigos e Referências
■
■ ■
Se suas classes possuem cada uma um conjunto completo de funções de acesso e mutantes, a única razão para tornar uma função amiga é fazer com que a função amiga seja mais simples e eficiente, mas em geral esta é uma razão suficiente. Uma referência é uma forma de nomear uma variável. É, essencialmente, um apelido ( alias ) para a variável. Quando se sobrecarregam os operadores >> ou<<, o tipo retornado deve ser stream e deve ser uma referência, o que é indicado pelo acréscimo de um & ao nome do tipo retornado.
RESPOSTAS DOS EXERCÍCIOS DE AUTOTESTE
1. A diferença entre um operador (binário), como +, * ou /) e uma função envolve a sintaxe de chamada. Em uma chamada de função, os argumentos são dados entre parênteses depois do nome da função. Com um operador, os argumentos são dados antes e depois do operador. Além disso, você deve utilizar a pala vra reservada operator na declaração do operador e na definição de um operador sobrecarregado. 2. Acrescente as seguintes declaração e definição de função: bool operator <(const Dinheiro& quantia1, const Dinheiro& quantia2); bool operator <(const Dinheiro& quantia1, const Dinheiro& quantia2)
{ int reais1 = quantia1.getReais( ); int reais2 = quantia2.getReais( ); int centavos1 = quantia1.getCentavos( ); int centavos2 = quantia2.getCentavos( ); return ((reais1 < reais2) ||
((reais1 == reais2) && (centavos1 < centavos2))); }
3. Quando se sobrecarrega um operador, pelo menos um dos argumentos do operador deve ser do tipoclasse. Isso impede a alteração do comportamento do + para inteiros. 4. Se você omitir o const no início da declaração e definição do operador de positivo sobrecarregado para a classe Dinheiro, a linha seguinte é legal: (m1 + m2) = m3;
Se a definição da classe Dinheiro for como mostrada no Painel 8.1, de forma que o operador positivo forneça por valor const, então não é legal. 5. const Dinheiro Dinheiro::operator -(const Dinheiro& segundoOperando) const { int totalCentavos1 = centavos + reais*100; int totalCentavos2 = segundoOperando.centavos
+ segundoOperando.reais*100; int difTotalCentavos = totalCentavos1 - totalCentavos2; int absTotalCentavos = abs(difTotalCentavos); int finalReais = absTotalCentavos/100; int finalCentavos = absTotalCentavos%100; if (difTotalCentavos < 0)
{ finalReais = -finalReais; finalCentavos = -finalCentavos; } return Dinheiro(finalReais, finalCentavos);
}
6. Uma função amiga e uma função-membro são semelhantes, no sentido de que ambas utilizam qualquer membro (público ou privado) em sua definição de função. Entretanto, uma função amiga é definida e utilizada como uma função ordinária; não se usa o operador ponto nem qualificadores de tipo quando se chama uma função amiga. Uma função-membro, por outro lado, é chamada por meio de um nome de
Respostas dos Exercícios de Autoteste
237
objeto e do operador ponto. Além disso, uma definição de função-membro inclui um qualificador de tipo, que consiste no nome da classe e no operador de resolução de escopo, ::. 7. //Utiliza cstdlib: const Dinheiro operator -(const Dinheiro& quantia1, const Dinheiro& quantia2)
{ int totalCentavos1 = int totalCentavos2 = int difTotalCentavos int absTotalCentavos
quantia1.centavos + quantia1.reais*100; quantia2.centavos + quantia2.reais*100; = totalCentavos1 - totalCentavos2; = abs(difTotalCentavos);
int finalReais = absTotalCentavos/100; int finalCentavos = absTotalCentavos%100; if (difTotalCentavos < 0)
{ finalReais = -finalReais; finalCentavos = -finalCentavos; } return Dinheiro(finalReais, finalCentavos);
}
8. Acrescente as seguintes declaração e definição de função: friend bool operator <(const Dinheiro& quantia1, const Dinheiro& quantia2); bool operator <(const Dinheiro& quantia1, const Dinheiro& quantia2)
{ return ((quantia1.reais < quantia2.reais) ||
((quantia1.reais == quantia2.reais) && (quantia1.centavos < quantia2.centavos))); }
9. Para entender por que não é circular, você precisa pensar na mensagem básica da sobrecarga: um único nome de função ou operador pode ter duas ou mais definições. Isso significa que dois ou mais operadores (ou funções) diferentes podem compartilhar um único nome. Na linha saidaStream << "R$-";
o operador << é o nome de um operador definido na biblioteca iostream para ser usado quando o segundo argumento é uma string entre aspas duplas. O operador chamado <<, que definimos no Painel 8.5, é um operador diferente que atua quando o segundo argumento é do tipo Dinheiro. 10. Se << e >> devem funcionar como queremos, o primeiro operando (primeiro argumento) deve ser cout ou cin (ou algum stream de arquivo de E/S). Mas se queremos sobrecarregar os operadores como membros, digamos, da classe Dinheiro, o primeiro operando terá de ser o objeto que faz a chamada e, assim, terá de ser do tipo Dinheiro, e não é o que desejamos. 11. //Utiliza iostream: istream& operator >>(istream& entradaStream, Porcentagem& umaPorcentagem) { char sinalDePorcentagem; entradaStream >> umaPorcentagem.valor; entradaStream >> sinalDePorcentagem;//Descarta o sinal %. return entradaStream; } //Utiliza iostream: ostream& operator <<(ostream& saidaStream,
238
Sobrecarga de Operador, Amigos e Referências const Porcentagem&
umaPorcentagem)
{ saidaStream << umaPorcentagem.valor << ’%’; return saidaStream;
}
12. É legal, mas o significado não é o que você poderia desejar. (a++) incrementa o valor das variáveis-membros em um, mas (a++)++ aumenta o valor das variáveis-membros em a++ em um, e a++ é um objeto diferente de a. (É possível definir o operador de incremento para que (a++)++ tenha o valor das variáveis-membros incrementado em dois, mas isso requer o uso do ponteiro this, que só será discutido no Capítulo 10.)
PROJETOS DE PROGRAMAÇÃO 1. Modifique a definição da classe Dinheiro mostrada no Painel 8.5 para acrescentar o seguinte: a. Os operadores <, <=, > e >= foram todos sobrecarregados para ser aplicados ao tipo Dinheiro. (Dica : veja Exercício de Autoteste 8.) b. A seguinte função-membro foi acrescentada à definição de classe. (Mostramos a declaração de função como deve aparecer na definição de classe. A definição da própria função incluirá o qualificador Dinheiro::.) const Dinheiro
porcentagem(int cifraPorcentagem)
const;
//Retorna uma porcentagem da quantia de dinheiro no objeto que faz a chamada. //Por exemplo, se cifraPorcentagem for 10, então o valor retornado é //10% da quantia de dinheiro representada pelo objeto que faz a chamada. Por exemplo, se carteira é um objeto de tipo Dinheiro cujo valor representa a quantia R$ 100,10, então a chamada
carteira.porcentagem(10);
retorna 10% de R$ 100,10; ou seja, retorna um valor de tipo Dinheiro que representa a quantia R$ 10,01. 2. Defina uma classe para números racionais. Um número racional é um número que pode ser representado como o quociente de dois inteiros. Por exemplo, 1/2, 3/4 e 64/2 são números racionais. (Com 1/2, etc., queremos dizer as frações comuns, não a divisão inteira que essa expressão produziria em um programa em C++.) Represente números racionais como dois valores de tipo int, um para o numerador e outro para o denominador. Chame a classe de Racional. Inclua um construtor com dois argumentos que possa ser usado para fixar as variáveis-membros de um objeto como qualquer valor legítimo. Inclua também um construtor que possua um único parâmetro de tipo int; chame esse único parâmetro de numeroInteiro e defina o construtor de modo que o objeto seja inicializado com o número racional numeroInteiro/1. Inclua um construtor-padrão que inicialize um objeto como 0 (ou seja, 0/1). Sobrecarregue os operadores de entrada e saída >> e <<. Os números devem entrar e sair na forma 1/2, 15/32, 300/401, e assim por diante. Observe que o numerador, o denominador ou ambos podem conter um sinal de menos, de modo que -1/2, 15/-32 e -300/-401 também são entradas possíveis. Sobrecarregue todos os seguintes operadores para que se apliquem corretamente ao tipo Racional: ==, <, <=, >, >=, +, -, * e /. Escreva um programateste para testar sua classe. Dicas: dois números racionais a/b e c/d são iguais se a*d é igual a c*b . Se b e d são números racionais positivos , a/b é menor que c/d desde que a*d seja menor que c*b . Inclua uma função para normalizar os valores armazenados de forma que, após a normalização, o denominador seja positivo e o numerador e o denominador sejam tão pequenos quanto possível. Por exemplo, depois da normalização, 4/-8 deve ser representado como -1/2. 3. Defina uma classe para números complexos. Um número complexo é um número da forma a + b*i
em que, para nossos propósitos, a e b são números de tipo double, e i é um número que representa a quantidade √− 1. Represente um número complexo como dois valores de tipo double. Chame as variáveismembros de real e imaginaria. (A variável para o número que é multiplicado por i é a que é chamada imaginaria.) Chame a classe de Complexo. Inclua um construtor com dois parâmetros de tipo double que possa ser usado para fixar em qualquer valor as variáveis-membros de um objeto. Inclua um construtor que possua apenas um parâmetro único de tipo double; chame esse parâmetro de parteReal e defina o construtor para que o objeto seja inicializado como parteReal + 0*i . Inclua um construtor-padrão que inicialize um objeto como 0 (ou seja, to 0 + 0*i ). Sobrecarregue todos os seguintes operadores para que
Projetos de Programação
239
se apliquem corretamente ao tipo Complexo: ==, +, -, *, >> e <<. Escreva, também, um programa-teste para testar sua classe. Dicas : para adicionar ou subtrair dois números complexos, adicione ou subtraia as duas variáveis-membros de tipo double. O produto de dois números complexos é dado pela seguinte fórmula: (a + b*i)*(c + d*i) == (a*c - b*d) + (a*d + b*c)*i
No arquivo de interface, defina uma constante i da seguinte forma: const Complexo
i(0, 1);
Esta constante definida i será a mesma que o i de que falamos anteriormente. 4. Modifique cumulativamente o exemplo do Painel 8.7 da seguinte forma: a. No Painel 8.7, substitua os membros privados char primeiro e segundo por um vetor de char de tamanho 100 e um membro de dados privado chamado tamanho. Inclua um construtor-padrão que inicialize tamanho como 10 e fixe as primeiras 10 posições de char como ’#’. (Utiliza apenas 10 das 100 posições possíveis.) Inclua uma função de acesso que forneça o valor do membro privado tamanho. Teste. b. Acrescente um operador[ ] membro que forneça um char& que permita ao usuário ter acesso a qualquer membro do vetor de dados privado ou o estabeleça utilizando um índice não-negativo que seja menor que tamanho. Teste. c. Acrescente um construtor que requeira um argumento int, tm, que fixe os primeiros tm membros do vetor char como ’#’. Teste. d. Acrescente um construtor que requeira um argumento int, tm, e um vetor de char de tamanho tm. O construtor deve fixar os primeiros tm membros do vetor de dados privado como os tm membros do vetor argumento de char. Teste. OBSERVAÇÕES: quando testar, utilize bons valores conhecidos, valores no limite e valores deliberadamente ruins. Não exigimos que você inclua verificações para índices fora dos limites em seu código, mas isso seria interessante. Alternativas para lidar com erros: envie uma mensagem de erro e depois "caia fora" (ou seja, chame exit(1)) ou dê ao usuário outra oportunidade de fornecer uma entrada correta.
Strings Strings
Capítulo 9Strings Polonius: O que o senhor está lendo? Hamlet: Palavras, palavras, palavras William Shakespeare, Hamlet
INTRODUÇÃO Este capítulo trata de dois tipos cujos valores representam strings de caracteres, como "Olá". Um tipo é apenas um vetor com tipo-base char que armazena strings de caracteres no vetor e assinala o fim da string com o caractere nulo, ’ \0’. Este é o modo antigo de se representar strings, que o C++ herdou da linguagem de programação C. Essas strings são chamadas strings C . Embora as strings C sejam um modo antigo de se representar strings, é difícil fazer qualquer espécie de processamento de strings em C++ sem um conhecimento mínimo de strings C. Por exemplo, strings de citação, como "Olá", são implementadas como strings C em C++. O padrão ANSI/ISO de C++ inclui um recurso mais moderno de se lidar com strings, na forma da classe string. A classe string é o segundo tipo string de que trataremos neste capítulo. A classe string plena utiliza templates (modelos) e é muito parecida com as classes templates da Standard Template Library (STL). Os templates serão abordados no Capítulo 16, e a STL, no Capítulo 19. Este capítulo trata dos usos básicos da classe string, que não requerem o conhecimento de templates. Este material não exige um conhecimento profundo de vetores, mas você deve estar habituado à notação vetorial básica, como a[i]. A Seção 5.1 do Capítulo 5 contém mais do que o necessário para que você leia este capítulo. Este material também não requer um conhecimento profundo de classes. A Seção 9.1, sobre strings C, e a Seção 9.2, sobre manipulação de caracteres, podem ser lidas antes dos Capítulos 6, 7 e 8, que tratam de classes. Entretanto, antes de ler a Seção 9.3, sobre a classe string padrão, você deve ler o Capítulo 6 e as seguintes partes do Capítulo 7: Seção 7.1 e a subseção da Seção 7.2, intitulada "Modificador de Parâmetros const" com a seção Armadilha que a acompanha.
9.1
Tipo Vetor para Strings Em tudo se deve levar em consideração o fim. Jean de La Fontaine, Fábulas, livro III (1668)
Esta seção descreve um modo de representar strings de caracteres, que o C++ herdou da linguagem C. A Seção 9.3 descreve uma classe string que é um modo mais moderno de se representar strings. Embora o tipo string descrito aqui possa ser um pouco "antiquado", ainda é amplamente utilizado e faz parte da linguagem C++.
242
Strings
VALORES STRING C E VARIÁVEIS STRING C Uma forma de representar uma string é como um vetor com tipo-base char. Entretanto, se a string for "Olá", é útil representá-la como um vetor de caracteres com quatro variáveis indexadas: três para as três letras de "Olá" mais uma para o caractere ’\0’, que serve como sinalizador de final. O caractere ’ \0’ é chamado de caractere nulo e é usado como sinalizador de final porque se distingue de todos os caracteres "reais". O sinalizador de final permite que o programa leia o vetor, um caractere de cada vez, e saiba que deve parar de ler quando lê o ’ \0’. Uma string armazenada dessa forma (como um vetor de caracteres terminado em ’ \0’) é chamada de string C. Escrevemos ’\0’ com dois símbolos em um programa, mas, assim como o caractere de nova linha,’ \n’, o caractere ’\0’ é, na verdade, um único valor de caractere. Como qualquer outro valor de caractere, ’ \0’ pode ser armazenado em uma variável de tipo char ou uma variável indexada de um vetor de caracteres. ■
O CARACTERE NULO, ’\0’ O caractere nulo, ’\0’, é utilizado para assinalar o final de uma string C armazenada em um vetor de caracteres. Quando um vetor de caracteres é usado dessa forma, costuma-se chamá-lo de variável string C. Embora o caractere nulo ’\0’ seja escrito com dois símbolos, é um caractere único que cabe em uma variável de tipo char ou uma variável indexada de um vetor de caracteres.
Você já vem utilizando strings C. Em C++, uma string literal, como "Olá", é armazenada como uma string C, embora você quase nunca precise ter consciência desse detalhe. Uma variável string C é apenas um vetor de caracteres. Assim, a seguinte declaração de vetor nos proporciona uma variável string C capaz de armazenar um valor string C com nove ou menos caracteres: char s[10];
O 10 é para as 9 letras na string mais o caractere nulo ’ \0’ para assinalar o final da string. Uma variável string C é um vetor de caracteres parcialmente preenchido. Como qualquer outro vetor parcialmente preenchido, uma variável string C utiliza posições a começar da variável indexada 0 até quantas forem necessárias. Entretanto, uma variável string C não utiliza uma variável int para controlar quanto do vetor é usado no momento. Em vez disso, coloca o símbolo especial ’ \0’ no vetor imediatamente após o último caractere da string C. Assim, se s contiver a string "Oi, mamãe!", os elementos do vetor são preenchidos como mostrado abaixo:
O caractere ’\0’ é utilizado com um valor de sentinela para marcar o final da string C. Se você ler os caracteres na string C começando da variável indexada s[0], seguindo para s[1], depois para s[2], e assim por diante, saberá que, ao encontrar o símbolo ’\0’, terá chegado ao fim da string C. Como o símbolo ’ \0’ sempre ocupa um elemento do vetor, o comprimento da string mais longa que o vetor pode abrigar é o tamanho do vetor menos um. O que distingue uma variável string C de um vetor de caracteres comum é que uma variável string C deve conter o caractere nulo ’ \0’ ao final do valor string C. Isso é uma distinção em relação a como o vetor é utilizado e não em relação ao que é o vetor. Uma variável string C é um vetor de caracteres, mas é usado de forma diferente. Pode-se inicializar uma variável string C na declaração, como ilustrado a seguir: char minhaMensagem[20] = "Olá, pessoal.";
Observe que a string C atribuída à variável string C não precisa preencher todo o vetor. Quando se inicializa uma variável string C, pode-se omitir o tamanho do vetor e o C++, automaticamente, fará o tamanho da variável string C com a extensão da string entre aspas mais um. (A variável indexada extra é para o ’\0’.) Por exemplo: char stringCurta[] = "abc";
é equivalente a char stringCurta[4] = "abc";
Tipo Vetor para Strings
243
DECLARAÇÃO DE VARIÁVEL STRING C Uma variável string C é o mesmo que um vetor de caracteres, mas é usada de forma diferente. Uma variável string C é declarada como um vetor de caracteres da forma usual.
SINTAXE char
Nome_Do_Vetor[Tamanho_Maximo_string_C + 1];
EXEMPLO char minhaStringC[11];
O + 1 abre espaço para o caractere nulo ’ \0’, que termina qualquer string C armazenada no vetor. Por exemplo, a variável string C minhaStringC, no exemplo acima, pode abrigar uma string C com dez ou menos caracteres de extensão.
Não confunda as inicializações: char stringCurta[]
= "abc";
e char stringCurta[]
= {’a’, ’b’, ’c’};
Não são equivalentes. A primeira dessas duas possíveis inicializações coloca o caractere nulo ’ \0’ no vetor após os caracteres ’a’, ’b’ e ’c’. A segunda não coloca um ’ \0’ em nenhum lugar do vetor.
INICIALIZANDO UMA VARIÁVEL STRING C Uma variável string C pode ser inicializada quando declarada, como ilustrado pelo seguinte exemplo: char suaString[11]
= "La Ra Ra";
Inicializar desta forma coloca automaticamente o caractere nulo, ’ \0’, no vetor ao final da string C especificada. Se você omitir o número dentro dos colchetes, [ ], a variável string C receberá o tamanho do comprimento da string C mais um. Por exemplo, a seguinte declaração de minhaString apresenta nove variáveis indexadas (oito para os caracteres da string C "La Ra Ra" e um para o caractere nulo ’ \0’): char minhaString[]
= "La Ra Ra";
Uma variável string C é um vetor, então possui variáveis indexadas que podem ser usadas como as de qualquer outro vetor. Por exemplo, suponha que seu programa contenha a seguinte declaração de variável string C: char nossaString[5]
= "Oi";
Com nossaString declarada como acima, seu programa possui as seguintes variáveis indexadas: nossaString[0], nossaString[1], nossaString[2], nossaString[3] e nossaString[4]. Por exemplo, o seguinte trecho alterará o valor string C em nossaString para uma string C de mesmo comprimento formada só de caracteres ’X’: int indice
= 0;
while (nossaString[indice]
!= ’\0’)
{ }
nossaString[indice] = ’X’; indice++;
Quando se manipulam essas variáveis indexadas, deve-se ter muito cuidado para não substituir o caractere nulo ’\0’ por algum outro valor. Se o vetor perder o valor ’ \0’, não se comportará mais como uma variável string C. No exemplo a seguir, o vetor felizString será alterado de modo que não contenha mais uma string C: char felizString[7]
= "LaRaRa"; felizString[6] = ’Z’;
Depois que o código acima é executado, o vetor felizString conterá ainda as seis letras na string C "LaRaRa", mas felizString não conterá mais o caractere nulo ’ \0’ para assinalar o fim da string C. Muitas funções de manipulação de strings dependem radicalmente da presença de ’ \0’ para assinalar o final do valor string C. Como outro exemplo, considere o loop while acima, que muda caracteres na variável string C nossaString. Esse loop while muda caracteres até encontrar um ’\0’. Se o loop nunca encontrar um ’ \0’, poderá alterar um
244
Strings
grande bloco de memória para valores indesejados, e o programa poderá começar a fazer coisas estranhas. Como medida de segurança, seria melhor reescrever o loop while acima da seguinte forma, para que, se o caractere nulo ’\0’ for perdido, o loop não altere inadvertidamente posições de memória além do final do vetor: int indice
= 0; while ( (nossaString[indice] != ’\0’) && (indice < TAMANHO) ) { nossaString[indice] = ’X’; indice++; } TAMANHO é
uma constante definida igual ao tamanho declarado do vetor nossaString.
BIBLIOTECA Você não precisa de nenhuma instrução de include nem utilizar um comando para declarar e inicializar strings C. Todavia, quando se processa strings C, é inevitável o uso de algumas das funções string predefinidas da biblioteca < cstring >. Assim, quando utilizar strings C, normalmente você fornecerá a seguinte instrução de include perto do início do arquivo que contém seu código: #include
As definições em estão colocadas no namespace global, não no std namespace, por isso não é necessária nenhuma instrução de using.
UTILIZANDO = E == COM STRINGS C Valores e variáveis string C não são como valores e variáveis de outros tipos de dados, e muitas das operações usuais não funcionam com strings C. Não se pode utilizar uma variável string C em uma declaração de atribuição utilizando =. Se você utilizar == para testar as strings C quanto à igualdade, não obterá o resultado esperado. O motivo desses problemas é que as strings C e as variáveis string C são vetores. Atribuir um valor a uma variável string C não é tão simples como com outros tipos de variáveis. O código que se segue é ilegal: char umaString[10];
Ilegal!
umaString = "Olá";
Embora se possa usar o sinal de igual para atribuir um valor a uma variável string C quando a variável é declarada, não se pode fazer isso em nenhum outro lugar do programa. Tecnicamente, o uso do sinal de igual em uma declaração, como em char felizString[7]
= "LaRaRa";
é uma inicialização, não uma atribuição. Se você quiser atribuir um valor a uma variável string C, deve fazer algo diferente. Existem diversas formas de se atribuir um valor a uma variável string C. O jeito mais fácil é usar a função predefinida strcpy, como mostrado a seguir: strcpy(umaString, "Olá");
Isso fixará o valor de umaString como igual a "Olá". Infelizmente, esta versão da função strcpy não faz verificações para assegurar que a cópia não ultrapasse o tamanho da variável string que é o primeiro argumento. Muitas versões de C++, mas não todas, também possuem uma versão de strcpy que requer um terceiro argumento que dá o número máximo de caracteres a serem copiados. Se esse terceiro parâmetro é fixado na posição do primeiro argumento como o tamanho da variável vetor menos um, então você obtém uma versão segura de strcpy (desde que a sua versão de C++ permita esse terceiro argumento). Por exemplo: char outraString[10];
strcpy(outraString, umaStringVariavel, 9);
Com esta versão de strcpy, no máximo nove caracteres (deixando espaço para o ’\0’) serão copiados da variável string C umaStringVariavel, independentemente de quão longa seja a string em umaStringVariavel. Também não se pode utilizar o operador == em uma expressão para testar se duas strings C são a mesma. (Na verdade, é pior do que isso. Pode-se utilizar == com strings C, mas isso não serve para testar se as strings C são iguais. Assim, se você usar == para testar duas strings C quanto à igualdade, corre o risco de obter resultados incorretos, e sem mensagem de erro!) Para testar se duas strings C são a mesma, pode-se usar a função predefinida strcmp. Por exemplo: if
(strcmp(stringC1, stringC2))
Tipo Vetor para Strings
245
cout << "As strings NÃO são iguais."; else
cout << "As strings são iguais."; Observe que a função strcmp atua de modo diferente do que você poderia supor. A comparação é verdadeira se as strings não são iguais. A função strcmp compara os caracteres nos argumentos da string C um de cada vez. Se em algum ponto a codificação numérica do caractere de stringC1 é menor que a codificação numérica do caractere correspondente de stringC2 , o teste pára nesse ponto e um número negativo é retornado. Se o caractere de stringC1 for maior que o caractere de stringC2, um número positivo é retornado. (Algumas implementações de strcmp retornam a diferença da codificação do caractere, mas não conte muito com isso.) Se as strings C forem iguais, um 0 é retornado. O relacionamento de ordem utilizado para comparar caracteres se chama ordem lexicográfica. É importante observar que se ambas as strings conti-
verem apenas letras maiúsculas ou apenas letras minúsculas, a ordem lexicográfica é a própria ordem alfabética. Como vimos, strcmp retorna um valor negativo, positivo ou zero, dependendo de as strings C comparadas lexicograficamente serem menores, maiores ou iguais. Se você utilizar strcmp como uma expressão booleana em um comando if ou em um looping para testar strings C quanto à igualdade, o valor não-zero será con vertido em true se as strings forem diferentes, e o zero será convertido em false. Não se esqueça dessa lógica invertida quando for testar strings C quanto à igualdade. Os compiladores de C++ que obedecem ao padrão dispõem de uma versão mais segura de strcmp, que possui um terceiro argumento que dá o número máximo de caracteres a serem comparados. As funções strcpy e strcmp estão na biblioteca com o arquivo de cabeçalho . Portanto, para utilizá-las você deve inserir a seguinte linha junto ao início do arquivo: #include As definições de strcpy e strcmp estão colocadas no namespace global, não no std namespace; por isso, a instrução de using não é necessária. ■
OUTRAS FUNÇÕES EM
O Painel 9.1 contém algumas das funções mais usadas da biblioteca com o arquivo de cabeçalho . Para utilizá-las, insira a seguinte linha junto ao início do arquivo: #include
Observe que coloca todas essas definições no namespace global, não no std namespace; por isso, não é necessária nenhuma instrução de using. Já falamos a respeito de strcpy e strcmp. A função strlen é fácil de entender e de usar. Por exemplo, strlen("larara") apresenta como saída 6, porque há seis caracteres em " larara". A função strcat é empregada para concatenar duas strings C; ou seja, para formar uma string mais longa colocando duas strings C mais curtas uma depois da outra. O primeiro argumento deve ser uma variável string C. O segundo argumento pode ser qualquer coisa que, avaliada, produza um valor string C, como uma string entre aspas duplas. O resultado é colocado na variável string C que é o primeiro argumento. Por exemplo, considere o seguinte código: char varString[20] = "O rato";
strcat(varString, "roeu");
Este código alterará o valor de varString para "O ratoroeu". Como este exemplo ilustra, é preciso ter o cuidado de levar em conta os espaços em branco quando se concatena strings C. Na tabela do Painel 9.1, você verá que existe uma versão mais segura, de três argumentos, da função strcat disponível em muitas, mas não todas, as versões de C++.
Painel 9.1 Algumas funções string C predefinidas em
(parte 1 de 2)
FUNÇÃO
DESCRIÇÃO
PRECAUÇÕES
strcpy(Var_String_Alvo, Src_String ) strncpy(Var_String_Alvo, Src_String , Limite)
Copia o valor string C Src_String na variável string C Var_String_Alvo. Semelhante à strcpy de dois argumentos, exceto pelo fato de que no máximo Limite caracteres são copiados.
Não verifica se Var_String_Alvo é grande o bastante para abrigar o valor Src_String . Se Limite for escolhido com cuidado, esta versão é mais segura que a strcpy de dois argumentos. Nem todas as versões de C++ têm essa função implementada.
246
Strings
Painel 9.1
Algumas funções string C predefinidas em (parte 2 de 2)
FUNÇÃO strcat(Var_String_Alvo, Src_String )
strncat(Var_String_Alvo, Src_String , Limite)
strlen( Src_String ) strcmp( String_1, String_2)
strncmp( String_1, String_2, Limite)
ARGUMENTOS
E
DESCRIÇÃO
PRECAUÇÕES
Concatena o valor string C Src_String ao final da string C na variável string C Var_String_Alvo. Semelhante à strcat de dois argumentos, exceto pelo fato de que no máximo Limite caracteres são copiados.
Não verifica se Var_String_Alvo é grande o bastante para abrigar o resultado da concatenação. Se Limite for escolhido com cuidado, esta versão é mais segura que a strcat de dois argumentos. Nem todas as versões de C++ têm essa função implementada.
Retorna um inteiro igual ao comprimento de Src_String . (O caractere nulo, ’ \0’, não é contado no comprimento.) Retorna 0 se String_1 e String_2 são iguais. Retorna um valor < 0 se String_1 for menor que String_2. Retorna um valor > 0 se String_1 for maior que String_2 (ou seja, retorna um valor não-zero se String_1 e String_2 forem diferentes). A ordem é lexicográfica. Semelhante à strcat de dois argumentos, exceto pelo fato de que no máximo Limite caracteres são comparados.
Se String_1 for igual a String_2, esta função apresenta como saída 0, que se converte em false. Observe que isto é o inverso do resultado que se poderia esperar quando as strings são iguais.
Se Limite for escolhido com cuidado, esta versão é mais segura que a strcat de dois argumentos. Nem todas as versões de C++ têm essa função implementada.
PARÂMETROS STRING C
Uma variável string C é um vetor, então um parâmetro string C é apenas um parâmetro de vetor. Da mesma forma que com qualquer parâmetro de vetor, sempre que uma função muda o valor de um parâmetro string C, é mais seguro incluir um parâmetro int adicional dando o tamanho declarado da variável string C. Por outro lado, se uma função utiliza apenas o valor em um argumento string C mas não altera esse valor, então não há necessidade de incluir outro parâmetro para dar o tamanho declarado da variável string C, nem quanto do vetor variável string C é preenchido. O caractere nulo, ’\0’, pode ser usado para detectar o final do valor da string C armazenado na variável string C.
1. Quais das seguintes declarações são equivalentes? char varString[10] = "Olá"; char varString[10] = {’O’, ’l’, ’á’, ’\0’}; char varString[10] = {’O’, ’l’, ’á’}; char varString[6] = "Olá"; char varString[] = "Olá";
2. Que string C será armazenada em stringMusical depois que o seguinte código é executado? char stringMusical[20] = "LaRaRa";
strcat(stringMusical, " para você");
Presuma que o código está inserido em um programa completo e correto e que existe uma instrução de include para no arquivo do programa. 3. O que há de errado com o seguinte código (se houver algo)? char varString[] = "Olá";
strcat(varString, " e Tchau."); cout << varString;
Presuma que o código está inserido em um programa completo e correto e que existe uma instrução de include para no arquivo do programa. 4. Suponha que a função strlen (que retorna o comprimento do argumento de sua string) ainda não ti vesse sido definida para você. Dê uma definição de função para strlen. Observe que strlen possui apenas um argumento, que é uma string C. Não acrescente outros argumentos, isso não é necessário.
Tipo Vetor para Strings
247
5. Qual é o comprimento (máximo) de uma string que pode ser colocada na variável string declarada pela seguinte declaração? Explique. char s[6];
6. Quantos caracteres há em cada uma das seguintes constantes caracteres e strings? a. ’\n’ b. ’n’ c. "Rita" d. "R" e. "Rita\n" 7. Como strings de caracteres são apenas vetores de char, por que o texto avisa para você não confundir declaração e inicialização? char stringCurta[]
= "abc"; char stringCurta[] = { ’a’, ’b’, ’c’};
8. Dadas as seguintes declaração e inicialização da variável string, escreva um loop para atribuir ’X’ a todas as posições dessa variável string, mantendo o comprimento igual. char nossaString[15]
= "Ei, você!";
9. Dada a declaração de uma variável string C, em que TAMANHO uma constante é definida: char nossaString[TAMANHO];
A variável string C nossaString foi atribuída por meio de um código não exibido aqui. Para corrigir variáveis string C, o seguinte loop reatribui a todas as posições de nossaString o valor ’ X’, deixando o comprimento igual a antes. Presuma que este fragmento de código esteja inserido em um programa que, de resto, está completo e correto. Responda às perguntas que se seguem a esse fragmento de código. int indice
= 0; while (nossaString[indice] != ’\0’) { nossaString[indice] = ’X’; indice++; }
a. Explique como esse código pode destruir os conteúdos da memória além do final do vetor. b. Modifique esse loop para proteger contra alterações indesejadas à memória além do final do vetor. 10. Escreva código utilizando uma função de biblioteca para copiar a constante string "Olá" para a variável string declarada a seguir. Não se esqueça de incluir ( #include) o arquivo de cabeçalho necessário para obter a declaração da função que você utiliza. char umaString[10];
11. Que string será fornecida quando este código é executado? (Presuma, como sempre, que este código está inserido em um programa completo e correto.) char musica[10]
= "I did it ";
char musicaDeSinatra[20];
strcpy ( musicaDeSinatra, musica ); strcat ( musicaDeSinatra, "my way!"); cout << musicaDeSinatra << endl;
12. Qual é o problema com este código (se houver algum)? char umaString[20]
= "Como vai você? "; strcat(umaString, "Bem, eu acho.");
■
ENTRADA E SAÍDA DE STRINGS C
Strings C podem ser enviadas para a saída com o operador de inserção, <<. Na verdade, já vínhamos fazendo isso com strings entre aspas duplas. Pode-se utilizar uma variável string C da mesma forma. Por exemplo, cout << noticia << " Uau.\n";
em que noticia é uma variável string C. É possível preencher uma variável string C por meio do operador de entrada >>, mas há algo que não deve ser esquecido. Como com todos os outros tipos de dados, todos os espaços em branco (espaços em branco, tabu-
248
Strings
lações e quebras de linha) são omitidos quando as strings C são lidas dessa forma. Além disso, cada leitura de entrada pára no próximo espaço ou quebra de linha. Por exemplo, considere o seguinte código: char a[80], b[80];
cout << "Digite alguma coisa:\n"; cin >> a >> b; cout << a << b << "FIM DA ENTRADA\n";
Quando inserido em um programa completo, esse código produz um diálogo como o seguinte: Digite alguma coisa: La ra ra para você!
LaraFIM DA ENTRADA
As variáveis string C a e b recebem cada uma apenas uma palavra da entrada: a recebe o valor string C "La", porque o caractere de entrada que se segue a La é um espaço em branco; b recebe "ra", porque o caractere de entrada que se segue a ra é um espaço em branco. Se você quiser que o programa leia uma linha inteira de entrada, pode utilizar o operador de extração, >>, para ler a linha, uma palavra de cada vez. Isso pode ser entediante e, mesmo assim, não lerá os espaços em branco na linha. Há um modo mais fácil de ler uma linha inteira de entrada e colocar a string C resultante em uma variável string C: utilize a função-membro predefinida getline, que é uma função-membro de cada stream de entrada (como cin ou um stream de arquivo de entrada). A função getline possui dois argumentos. O primeiro é uma variável string C para receber a entrada e o segundo é um inteiro que em geral é o tamanho declarado da variável string C. O segundo argumento especifica o número máximo de elementos de vetor na variável string C que getline será autorizada a preencher com caracteres. Por exemplo, considere o seguinte código: char a[80];
cout << " Digite alguma coisa:\n"; cin.getline(a, 80); cout << a << " FIM DA ENTRADA \n";
Quando inserido em um programa completo, esse código produz um diálogo como o que se segue: Digite alguma coisa: La ra ra para você!
La ra ra para você!FIM DA ENTRADA
Com a função cin.getline, a linha inteira é lida. A leitura termina quando a linha termina, mesmo que a string C resultante possa ser menor que o número máximo de caracteres especificado pelo segundo argumento. Quando getline é executada, a leitura pára depois que o número de caracteres dado pelo segundo argumento tenha sido preenchido no vetor string C, mesmo que o fim da linha não tenha sido alcançado. Por exemplo, considere o código seguinte: char stringCurta[5];
cout << "Digite alguma coisa:\n"; cin.getline(stringCurta, 5); cout << stringCurta << "FIM DA ENTRADA\n";
Quando inserido em um programa completo, esse código produz um diálogo como o que se segue: Digite alguma coisa: larararam
laraFIM DA ENTRADA
Observe que quatro, não cinco, caracteres são lidos para a variável string C stringCurta, ainda que o segundo argumento seja 5. Isso acontece porque o caractere nulo ’ \0’ preenche uma posição no vetor. Cada string C termina com o caractere nulo quando é armazenada em uma variável string C, e isso sempre consome uma posição de vetor. As técnicas de entrada e saída de strings C que ilustramos para cout e cin funcionam da mesma forma com arquivos de entrada e saída. O stream de entrada cin pode ser substituído por um stream de entrada conectado a um arquivo. O stream de saída cout pode ser substituído por um stream de saída conectado a um arquivo. (A E/S de arquivos será discutida no Capítulo 12.)
Ferramentas de Manipulação de Caracteres
249
getline A função-membro getline pode ser utilizada para ler uma linha de entrada e colocar a string de caracteres dessa linha em uma variável string C. SINTAXE cin.getline(String_Var, Max_Caracteres + 1);
Uma linha de entrada é lida do stream Input_Stream e a string C resultante é colocada em String_Var. Se a linha é maior que Max_Caracteres , apenas os primeiros Max_Caracteres na linha são lidos. (O +1 é necessário porque toda string C tem o caractere nulo ’\0’ acrescentado ao final da string C e, assim, a string armazenada em String_Var é um caractere mais longo que o número de caracteres lidos.)
EXEMPLO char umaLinha[80];
cin.getline(umaLinha, 80);
Como você verá no Capítulo 12, pode-se utilizar um stream de entrada ligado a um arquivo de texto em lugar de cin.
13. Considere o código seguinte (suponha que esteja inserido em um programa completo e correto e o execute): char a[80], b[80]; cout << "Digite alguma coisa:\n";
cin >> a >> b; cout << a << ’-’ << b << "FIM DA ENTRADA\n";
Se o diálogo se iniciar da forma a seguir, qual será a próxima linha de saída? Digite alguma coisa: A hora é agora.
14. Considere o código seguinte (suponha que esteja inserido em um programa completo e correto e o execute): char minhaString[80];
cout << "Digite uma linha de entrada:\n"; cin.getline(minhaString, 6); cout << minhaString << "
Se o diálogo se iniciar da seguinte forma, qual será a próxima linha de saída? Digite uma linha de entrada: Que os pêlos dos seus dedos do pé fiquem longos e encaracolados.
9.2
Ferramentas de Manipulação de Caracteres Eles escrevem Vinci e pronunciam Vintchi; os estrangeiros sempre escrevem melhor do que pronunciam. Mark Twain, The Innocents Abroad
Qualquer forma de string é, em última análise, composta de caracteres individuais. Assim, quando se faz o processamento de strings, muitas vezes é interessante ter ferramentas à disposição para testar e manipular valores individuais de tipo char. Esta seção trata de tais ferramentas. ■ E/S DE CARACTERES
Todos os dados entram e saem como dados caracteres. Quando seu programa apresenta como saída o número 10, na realidade são os dois caracteres ’ 1’ e ’0’ que são a saída. De forma similar, quando o usuário quer digitar o número 10, digita o caractere ’ 1’ seguido pelo caractere ’ 0’. Se o computador interpreta esse " 10" como dois caracteres ou como o número 10, depende de como seu programa foi escrito. Mas, como quer que tenha sido escri-
250
Strings
to, o hardware do computador está sempre lendo os caracteres ’ 1’ e ’0’, não o número 10. Essa conversão entre caracteres e números em geral é automática, de modo que você não precisa se preocupar com tais detalhes; entretanto, às vezes toda essa ajuda automática cria dificuldades. Por isso, o C++ fornece alguns recursos de baixo-nível para a entrada e saída de dados caracteres. Esses recursos de baixo-nível não incluem conversões automáticas. Isso permite que se contornem os recursos automáticos e se faça entrada/saída do jeito que se desejar. Você pode até escrever funções de entrada e saída que leiam e escrevam valores int em notação de números romanos, se quiser ser realmente perverso. ■
FUNÇÕES-MEMBROS get E put
A função get permite que seu programa leia um caractere de entrada e o armazene em uma variável de tipo Cada stream de entrada, quer seja um stream de arquivo de entrada ou o stream cin, possui get como uma função-membro. Descreveremos get aqui como uma função-membro do objeto cin. (Quando discutirmos E/S de arquivos no Capítulo 12, veremos que se comporta da mesma forma para streams de arquivo de entrada que para cin.) Até agora, utilizamos cin com o operador de extração, >>, a fim de ler um caractere de entrada (ou qualquer outra entrada, na realidade). Quando se usa o operador de extração >>, algumas coisas são feitas para você automaticamente, como ignorar espaços em branco. Mas às vezes você não quer ignorar espaços em branco. A funçãomembro cin.get lê o caractere de próxima entrada sem se importar se esse caractere é um dos caracteres de espaço em branco (espaço em branco, tabulação ou quebra de linha) ou não. A função-membro get requer um argumento, que deve ser uma variável de tipo char. Esse argumento recebe o caractere de entrada que é lido do stream de entrada. Por exemplo, o seguinte código lerá o próximo caractere de entrada a partir do teclado e o armazenará na variável proximoSimbolo: char.
char proximoSimbolo; cin.get(proximoSimbolo);
É importante observar que seu programa pode ler qualquer caracter dessa forma. Se o próximo caractere de entrada é um espaço em branco, esse código lerá o caractere de espaço em branco. Se o próximo caractere for o caractere de nova linha ’ \n’ (ou seja, se o p chegou ao final de uma linha de entrada), então a chamada acima a cin.get fixará o valor de proximoSimbolo como igual a ’\n’. Por exemplo, suponha que seu programa contenha o seguinte código: char c1, c2, c3;
cin.get(c1); cin.get(c2); cin.get(c3);
e suponha que você digite as duas seguintes linhas de entrada para serem lidas por esse código: AB CD
O valor de c1 é fixado como ’A’, o valor de c2 é fixado como ’B’ e o valor de c3 é fixado como ’\n’. A variá vel c3 não é fixada como igual a ’C’. Algo que você pode fazer com a função-membro get é mandar seu programa detectar o final de uma linha. O seguinte loop lerá uma linha de entrada e parará depois de passar pelo caractere de nova linha ’ \n’. Qualquer entrada subseqüente lerá a partir do início da próxima linha. Para este primeiro exemplo, simplesmente ecoamos a entrada, mas a mesma técnica permitiria que você fizesse o que quisesse com a entrada. cout << "Forneça uma linha de entrada e eu a ecoarei:\n"; char simbolo; do
{
cin.get(simbolo); cout << simbolo; } while (simbolo != ’\n’); cout << "Fim da demonstração.\n";
Ferramentas de Manipulação de Caracteres
251
Este loop lerá qualquer linha de entrada e a ecoará com exatidão, incluindo os espaços em branco. Eis o diálogo-exemplo produzido por esse código: Forneça uma linha de entrada e eu a ecoarei: La Ra Ra 1 2 34 La Ra Ra 1 2 34 Fim da demonstração.
Observe que o caractere de nova linha ’ \n’ tanto é lido quanto fornecido. Como ’ \n’ é saída, a string que começa com a palavra "Fim" está em uma nova linha. ’\n’ E "\n" ’\n’ e "\n" às vezes parecem a mesma coisa. Em um comando cout, ambos produzem o mesmo efeito, mas não podem ser usados como equivalentes em todas as situações. ’ \n’ é um valor de tipo char e pode ser armazenado em uma variável de tipo char. Por outro lado, "\n" é uma string constituída por exatamente um caractere. Assim, "\n" não é de tipo char e não pode ser armazenada em uma variável de tipo char.
A função-membro put é análoga à função-membro get, só que é usada para saída em vez de para entrada. A função put permite que seu programa apresente um caractere como saída. A função-membro cout.put requer um argumento, que deve ser uma expressão de tipo char, como uma constante ou uma variável de tipo char. O valor do argumento é apresentado na tela quando a função é chamada. Por exemplo, a linha seguinte apresentará como saída na tela a letra ’a’: cout.put(’a’);
FUNÇÃO-MEMBRO
get A função get pode ser utilizada para ler um caractere de uma entrada. Diferentemente do operador de extração, >>, get lê o próximo caractere de entrada, independentemente do que esse caractere seja. Em particular, get lerá um caractere de espaço em branco ou de nova-linha, ’\n’, se estes forem o próximo caractere de entrada. A função get requer um argumento, que deve ser uma variável de tipo char. Quando get é chamada, o próximo caractere de entrada é lido e a variável do argumento tem seu valor fixado como igual a esse caractere de entrada.
EXEMPLO char proximoSimbolo;
cin.get(proximoSimbolo);
Como veremos no Capítulo 12, quando se deseja utilizar get para ler de um arquivo, emprega-se um stream de arquivo de entrada no lugar do stream cin.
A função cout.put não permite que se faça nada que não se poderia fazer com o operador de inserção <<, mas a incluímos aqui em nome da abrangência. (Quando tratarmos de E/S de arquivo, no Capítulo 12, veremos que put pode ser usada com um stream de saída conectado a um arquivo de texto e não se restringe ao uso apenas com cout.) Se seu programa utiliza cin.get ou cout.put, então, como com outros usos de cin e cout, seu programa deve incluir uma das seguintes linhas (ou algo similar): #include using namespace std;
ou #include using std::cin; using std::cout;
252
Strings
VERIFICANDO ENTRADA POR MEIO DE UMA FUNÇÃO DE NOVA LINHA A função getInt, no Painel 9.2, pergunta ao usuário se a entrada está correta e pede um novo valor se o usuário disser que está incorreta. O programa no Painel 9.2 é apenas um programa-driver para testar a função getInt, mas a função, ou outra bastante similar a ela, pode ser usada em quase todo tipo de programa que retire sua entrada a partir do teclado. Observe a chamada à função novaLinha( ). A função novaLinha lê todos os caracteres no resto da linha atual, mas não faz nada com eles. Isso equivale a descartar o resto da linha. Assim, se o usuário digitar No, o programa lê a primeira letra, que é N, e depois chama a função novaLinha, que descarta o resto da linha de entrada. Isso significa que, se o usuário digitar 75 na próxima linha de entrada, como mostra o diálogo programa-usuário, o programa lerá o número 75 e não tentará ler a letra o na palavra No. Se o programa não incluir uma chamada à função novaLinha, então o próximo item lido será o o na linha que contém No em vez do número 75 na linha seguinte.
Painel 9.2
1 2 3
Verificando entrada ( parte 1 de 2)
//Programa para demonstrar as funções newLine e getInput. #include using namespace std;
4 void newLine( ); 5 //Descarta todas as entradas remanescentes na linha de entrada atual. 6 //Descarta também os ‘\n’ do final da linha. 7 void getInt(int& number); 8 //Fixa número da variável como um 9 //valor que o usuário aprova.
10 int main( ) 11 { 12 int n; 13 14 15
getInt(n); cout << "Valor final lido em = " << n << endl << "Final da demonstração.\n";
16 17 }
return 0;
18 //Utiliza iostream: 19 void newLine( ) 20 { char symbol; 21 do 22 23 { 24 cin.get(symbol); 25 } while (symbol != ’\n’); 26 } 27 //Utiliza iostream: 28 void getInt(int& number) 29 { char ans; 30 31 do 32 { 33 cout << "Digite o número de entrada: "; 34 cin >> number; 35 cout << "Você digitou " << number 36 << " Isso está correto? (sim/não): ";
Ferramentas de Manipulação de Caracteres Painel 9.2
37 38 39 40 }
253
Verificando entrada ( parte 2 de 2)
cin >> ans; newLine( ); } while ((ans == ’N’) || (ans == ’n’));
DIÁLOGO PROGRAMA-USUÁRIO Digite o número de entrada: 57 Você digitou 57. Isso está correto? (sim/não): Não Não Não! Digite o número de entrada: 75 Você digitou 75. Isso está correto? (sim/não): sim Valor final lido em = 75 Final da demonstração.
’\n’ INESPERADO NA ENTRADA
Quando se utiliza a função-membro get é preciso contar cada caractere da entrada, mesmo os caracteres que você não considera como símbolos, como os espaços em branco e o caractere de nova linha, ’\n’. Um problema comum quando se utiliza get é esquecer de se desfazer do ’ \n’ que termina cada linha de entrada. Se houver um caractere de nova linha no stream de entrada que não seja lido (e normalmente descartado), então quando seu programa espera ler a seguir um símbolo "real" utilizando a função-membro get, em vez disso, leremos o caractere ’\n’. Para limpar o stream de entrada de qualquer ’\n’ restante, você pode utilizar a função novaLinha, que definimos no Painel 9.2 (ou pode utilizar a função ignore, que discutiremos na próxima subseção). Vamos dar uma olhada em um exemplo concreto. É correto mistura as diferentes formas de cin. Por exemplo, o seguinte código é legal: cout << "Digite um número:\n"; int numero;
cin >> numero; cout << "Agora digite uma letra:\n"; char simbolo; cin.get(simbolo);
Entretanto, isto pode causar problemas, como o ilustrado pelo seguinte diálogo: Digite um número: 21
Agora digite uma letra: A
Com este diálogo, o valor de numero será 21, como esperado. Todavia, se você espera que o valor da variá vel simbolo seja ’A’, ficará desapontado. O valor dado a simbolo é ’\n’. Depois de ler o número 21, o próximo caractere no stream de entrada é o caractere de nova linha, ’ \n’, e, portanto, ele é lido a seguir. Lembre-se de que get não ignora quebras de linha e espaços. (Na realidade, dependendo do que houver no restante do programa, você pode até nem ter a oportunidade de digitar o A. Uma vez que a variável simbolo seja preenchida com o caractere ’\n’, o programa avança para o próximo comando. Se o próximo comando enviar saída para a tela, a tela será preenchida com a saída antes que você possa digitar o A.) Se reescrevermos o código acima desta forma, faremos com que o diálogo acima preencha a variável numero com 21 e a variável simbolo com ’A’: cout << " Digite um número:\n"; int numero;
cin >> numero; cout << "Agora digite uma letra:\n"; char simbolo; cin >> simbolo; Uma outra opção é utilizar a função novaLinha, cout << " Digite um número:\n"; int numero;
cin >> numero; novaLinha( ); cout << "Agora digite uma letra:\n";
definida no Painel 9.2, assim:
254
Strings
char simbolo; cin.get(simbolo);
Como esta segunda versão indica, podem-se misturar as duas formas de cin e fazer o programa funcionar corretamente, mas isso requer alguns cuidados extras. Como uma terceira alternativa, pode-se usar a função ignore, de que falaremos na próxima subseção.
■
FUNÇÕES-MEMBRO putback , peek E ignore
Às vezes seu programa precisa conhecer o próximo caractere no stream de entrada. Entretanto, depois de ler o próximo caractere, pode ser que você não queira processá-lo e prefira "devolvê-lo". Por exemplo, se você quiser que seu programa leia até o primeiro espaço em branco que encontrar mas não o inclua, seu programa deve ler esse primeiro espaço em branco a fim de saber quando parar de ler — mas, depois, esse espaço em branco não está mais no stream de entrada. Alguma outra parte de seu programa pode precisar ler e processar esse espaço em branco. Uma forma de lidar com essa situação é utilizar a função-membro cin.putback. A função cin.putback requer um argumento de tipo char e coloca o valor desse argumento de volta no stream de entrada de forma que seja o próximo caractere a ser lido. O argumento pode ser qualquer expressão que seja avaliada como um valor de tipo char. O caractere colocado de volta no stream de entrada com a função putback não precisa ser o último caractere lido; pode ser qualquer caractere que se deseje. A função-membro peek faz o que o nome indica ( to peek significa "espiar"). cin.peek( ) retorna o próximo caractere a ser lido por cin, mas não utiliza esse caractere; a próxima leitura começa com esse caractere. Em outras palavras, a função peek "espia" e conta ao seu programa qual é o próximo caractere a ser lido. Se você quiser ignorar entradas até algum caractere designado, como o caractere de nova linha ’ \n’, empregue a função-membro ignore. O código seguinte ignorará todos os caracteres de entrada até o caractere de nova linha, ’\n’, (inclusive): cin.ignore(1000, ’\n’);
No caso, 1000 é o número máximo de caracteres a ignorar. Se o delimitador, nesse caso ’ \n’, não for encontrado depois de 1.000 caracteres, então nenhum outro caractere é ignorado. Claro que um argumento int diferente pode ser utilizado em vez de 1000, e um caractere diferente pode ser utilizado como argumento no lugar de ’ \n’. Como veremos no Capítulo 12, as funções-membros putback, peek e ignore podem ser usadas com cin substituído por um objeto de stream de arquivo de entrada para uma entrada de arquivo de texto.
15. Considere o seguinte código (suponha que esteja inserido em um programa completo e correto e o execute): char c1, c2, c3, c4;
cout << "Digite uma linha de entrada:\n"; cin.get(c1); cin.get(c2); cin.get(c3); cin.get(c4); cout << c1 << c2 << c3 << c4 << "FIM DA ENTRADA";
Se o diálogo se iniciar da seguinte forma, qual será a próxima linha de saída? Digite uma linha de entrada: a b c d e f g
16. Considere o seguinte código (suponha que esteja inserido em um programa completo e correto e o execute): char proximo; int contagem = 0;
cout << "Digite uma linha de entrada:\n"; cin.get(proximo);
Ferramentas de Manipulação de Caracteres
255
while (proximo != ’\n’)
{ if ((contagem%2) == 0)
}
Verdadeiro se contagem for par
cout << proximo; contagem++; cin.get(proximo);
Se o diálogo se iniciar da seguinte forma, qual será a próxima linha de saída? Digite uma linha de entrada: abcdef gh
17. Suponha que o programa descrito no Exercício de Autoteste 16 seja executado e que o diálogo se inicie da seguinte forma (em vez de iniciar como mostrado no Exercício de Autoteste 16). Qual será a próxima linha de saída? Digite uma linha de entrada: 0 1 2 3 4 5 6 7 8 9 10 11
18. Considere o código seguinte (suponha que esteja inserido em um programa completo e correto e o execute): char proximo; int contagem = 0;
cout << "Digite uma linha de entrada:\n"; cin >> proximo; while (proximo != ’\n’) { if ((contagem%2) == 0) cout << proximo; contagem++; cin >> proximo; }
Se o diálogo se iniciar da seguinte forma, qual será a próxima linha de saída? Digite uma linha de entrada: 0 1 2 3 4 5 6 7 8 9 10 11
■
FUNÇÕES DE MANIPULAÇÃO DE CARACTERES
Em processamento de texto muitas vezes se deseja converter letras minúsculas em maiúsculas ou vice-versa. A função predefinida toupper converte uma letra minúscula em maiúscula. Por exemplo, toupper(’a’) apresenta como saída ’A’. Se o argumento da função toupper é algo que não seja uma letra minúscula, toupper simplesmente retorna o argumento inalterado. Assim, toupper(’A’) apresenta como saída ’ A’ e toupper(’?’) apresenta como saída ’?’. A função tolower faz a operação inversa: converte uma letra maiúscula em minúscula. As funções toupper e tolower estão na biblioteca com o arquivo de cabeçalho ; portanto, qualquer programa que as utilize deve conter a seguinte instrução: #include
Observe que coloca todas as definições namespace global e, assim, não é necessária nenhuma instrução de using. O Painel 9.3 contém descrições de algumas das funções mais usadas na biblioteca .
Painel 9.3 Algumas funções em
(parte 1 de 2)
FUNÇÃO
DESCRIÇÃO
EXEMPLO
toupper(Exp_Char)
Retorna a versão em letra maiúscula de Exp_Char (como um valor de tipo int).
char c = toupper(’a’);
Retorna a versão em letra minúscula de Exp_Char (como um valor de tipo int).
char c = tolower(’A’);
tolower(Exp_Char)
cout << c; Saída: A cout << c; Saída: a
256
Strings
Painel 9.3
Algumas funções em (parte 2 de 2)
FUNÇÃO
DESCRIÇÃO
EXEMPLO
isupper(Exp_Char)
Retorna true desde que Exp_Char seja uma letra maiúscula; caso contrário, retorna false.
if (isupper(c))
cout << "É maiúscula."; else
cout << "Não é maiúscula."; islower(Exp_Char)
Retorna true desde que Exp_Char seja uma letra minúscula; caso contrário, retorna false.
char c = ’a’; if (islower(c))
cout << c << " é minúscula."; a é minúscula.
Saída:
isalpha(Exp_Char)
Retorna true desde que Exp_Char seja uma letra do alfabeto; caso contrário, retorna false.
char c = ’$’; if (isalpha(c))
cout << "É uma letra."; else
cout << "Não é uma letra."; é uma letra.
Saída: Não
isdigit(Exp_Char)
Retorna true desde que Exp_Char seja um dígito de ’0’ a ’9’; caso contrário, retorna false.
if (isdigit(’3’))
cout << "É um dígito."; else
cout << "Não é um dígito."; um dígito.
Saída: É
isalnum(Exp_Char)
Retorna true desde que Exp_Char seja uma letra ou um dígito; caso contrário, retorna false.
if (isalnum(’3’)
&& isalnum(’a’)) cout << "Os dois são alfanuméricos.";
else
cout << "Um ou mais não é."; Saída: Os dois são alfanuméricos. isspace(Exp_Char)
Retorna true desde que Exp_Char seja um caractere de espaço em branco, de nova linha ou de tabulação; caso contrário, retorna false.
//Ignora uma "palavra" e fixa c //como igual ao primeiro caractere de espaço em branco, nova linha ou tabulação //depois da "palavra": do
ispunct(Exp_Char)
isprint(Exp_Char)
isgraph(Exp_Char)
isctrl(Exp_Char)
Retorna true desde que Exp_Char seja um caractere de impressão e não um caractere de espaço em branco, nova linha, tabulação dígito ou letra; caso contrário, retorna false. Retorna true desde que Exp_Char seja um caractere de impressão; caso contrário, retorna false. Retorna true desde que Exp_Char seja um caractere de impressão diferente do caractere de espaço em branco, de nova linha ou tabulação; caso contrário, retorna false. Retorna true desde que Exp_Char seja um caractere de controle; caso contrário, retorna false.
{ cin.get(c); } while (! isspace(c)); if (ispunct(‘?’)) cout<< É pontuação."; else
cout << "Não é pontuação." ;
A função isspace retorna true se seu argumento é um caractere de espaço em branco, de nova linha ou de tabulação. Se o argumento de isspace não for um desses caracteres, então isspace retorna false. Assim, isspace(’ ’) retorna true e isspace(’a’) retorna false. Por exemplo, o código seguinte lerá uma sentença terminada com um ponto final e ecoará a string com todos os caracteres de espaço em branco, nova linha ou tabulação substituídos pelo símbolo ’ -’: char proximo; do
Ferramentas de Manipulação de Caracteres {
257
cin.get(proximo); if (isspace(proximo)) cout << ’-’;
else cout << proximo; } while (proximo != ’.’);
Por exemplo, se o código acima receber a seguinte entrada: Ahh
la ra ra.
produzirá a seguinte saída: Ahh---la-ra-ra.
toupper E tolower RETORNAM VALORES int Muitas vezes o C++ considera os caracteres como números inteiros, semelhantes aos números de tipo int. A cada caractere é atribuído um número. Quando o caractere é armazenado em uma variável de tipo char, esse número é colocado na memória do computador. Em C++, pode-se usar um valor de tipo char como um número, por exemplo, colocando-o em uma variável de tipo int. Também se pode armazenar um número de tipo int em uma variável de tipo char (desde que o número não seja muito grande). Assim, o tipo char pode ser utilizado como o tipo para caracteres ou como o tipo para números inteiros pequenos. Normalmente você não precisa se incomodar com esse detalhe e pode pensar que os valores de tipo char são caracteres, sem se preocupar com seu uso numérico. Entretanto, quando se utilizam algumas das funções em , esse detalhe pode ser importante. As funções toupper e tolower na realidade retornam valores de tipo int e não valores de tipo char; ou seja, retornam o número correspondente ao caractere que pensamos que estão retornando, em vez do próprio caractere. Assim, o código seguinte não apresenta como saída a letra ’ A’, e sim o número que é atribuído a ’A’: cout << toupper(’a’);
Para fazer com que o computador trate o valor retornado por toupper ou tolower como um valor de tipo char (como oposto ao valor de tipo int), você precisa indicar que deseja um valor de tipo char. Uma forma de fazer isso é colocar o valor retornado em uma variável de tipo char. O seguinte trecho apresentará como saída o caractere ’A’, que normalmente é o que desejamos: char c = toupper(’a’); cout << c;
Outra forma de se fazer o computador tratar o valor retornado por toupper ou tolower como um valor de tipo char é utilizar uma conversão de tipo ( casting ): cout << static_cast (toupper(’a’));
19. Considere o código seguinte (suponha que esteja inserido em um programa completo e correto e o execute): cout << "Digite uma linha de entrada:\n"; char proximo;
do {
cin.get(proximo); cout << proximo; } while ( (! isdigit(proximo)) && (proximo != ’\n’) ); cout << "
Se o diálogo se iniciar da seguinte forma, qual será a próxima linha de saída? Digite uma linha de entrada:
Vejo você às 10h30. 20. Escreva código em C++ que leia uma linha de texto e a ecoe com todas as letras maiúsculas eliminadas. 21. Reescreva a definição da função novaLinha no Painel 9.2, mas, dessa vez, utilize a função-membro ignore.
258
Strings
9.3
Classe-Padrão string Tento captar cada frase, cada palavra que você e eu dizemos, e logo encerro todas essas frases e palavras em meu depósito literário, porque um dia elas podem ser úteis. Anton Chekhov, A Gaivota
A Seção 9.1 apresentou as strings C. Essas strings C são apenas vetores de caracteres terminados pelo caractere nulo, ’\0’. Para manipular essas strings C você precisa se preocupar com todos os detalhes da manipulação de vetores. Por exemplo, quando você quer acrescentar caracteres a uma string C e não há espaço suficiente no vetor, você precisa criar outro vetor para guardar essa string de caracteres maior. Em suma, as strings C exigem que o programador controle todos os detalhes de baixo-nível de como as strings C são armazenadas na memória. Isso requer muito trabalho extra e é uma fonte de erros. O padrão ANSI/ISO de C++ estabeleceu que o C++ deve agora ter uma classe string que permita ao programador tratar as strings como um tipo de dados básico, sem precisar se preocupar com a implementação de detalhes. Esta seção apresenta a você esse tipo string. ■
INTRODUÇÃO À CLASSE-PADRÃO string
A classe string está definida na biblioteca cujo nome também é , e as definições estão colocadas no std namespace. Para utilizar a classe string, portanto, seu código deve conter a seguinte instrução (ou algo mais ou menos equivalente): #include using namespace std;
A classe string permite que você trate valores e expressões string de forma bem semelhante àquela como trata valores de um tipo simples. Pode-se utilizar o operador = para atribuir um valor a uma variável string e podese utilizar o sinal + para concatenar duas strings. Por exemplo, suponha que s1, s2 e s3 são objetos de tipo string e tanto s1 quanto s2 possuem valores string. Então s3 pode ser fixada como igual à concatenação do valor string em s1 seguido pelo valor string em s2, da seguinte forma: s3 = s1 + s2;
Não há perigo de s3 ser pequeno demais para seu novo valor string. Se a soma dos comprimentos de s1 e s2 exceder a capacidade de s3, mais espaço é alocado automaticamente para s3. Como observamos anteriormente neste capítulo, strings entre aspas duplas são, na verdade, strings C e, assim, não são exatamente de tipo string. Entretanto, o C++ fornece conversão de tipo automática ( casting ) de strings entre aspas duplas para valores de tipo string. Assim, podem-se utilizar strings entre aspas duplas como se fossem valores literais de tipo string, e nós (e a maioria dos outros autores) sempre nos referiremos a strings entre aspas duplas como se fossem valores de tipo string. Por exemplo, s3 = "Olá Mãe!";
fixa o valor da variável string s3 como um objeto string com os mesmos caracteres que a string C " Olá Mãe!". A classe string possui um construtor padrão que inicializa um objeto string com a string vazia. A classe string também possui um segundo construtor que requer um argumento que é uma string C padrão e, portanto, pode ser uma string entre aspas duplas. Este segundo construtor inicializa o objeto string com um valor que representa a mesma string que seu argumento string C. Por exemplo, string frase; string substantivo("formiga");
A primeira linha declara a variável string frase e a inicializa com a string vazia. A segunda linha declara substantivo como de tipo string e o inicializa com um valor string equivalente à string C "formiga". A maioria dos programadores, em conversas informais, diz que "substantivo foi inicializado como "formiga", mas, na verdade, existe aí uma conversão de tipo. A string entre aspas duplas "formiga" é uma string C, não um valor de tipo string. A variável substantivo recebe um valor string que possui os mesmos caracteres de "formiga" na mesma ordem que em "formiga’, mas o valor string não termina com o caractere nulo ’\0’. Em teoria, pelo menos, você não precisa saber nem se preocupar se o valor string de substantivo é armazenado em um vetor, ao contrário de outras estruturas de dados.
Classe-Padrão string
259
Há uma notação alternativa para se declarar uma variável string e invocar o construtor-padrão. As duas linhas seguintes são perfeitamente equivalentes: string substantivo("formiga"); string substantivo = "formiga";
Esses detalhes básicos sobre a classe string são ilustrados no Painel 9.4. Observe que, como ilustrado ali, você pode enviar valores string para a saída por meio do operador <<. Considere a seguinte linha do Painel 9.4: frase = "Eu adoro " + substantivo + " " + adjetivo + "!"; Painel 9.4
Programa utilizando a classe string //Demonstra a classe-padrão string #include #include using namespace std;
1 2 3 4 5 6 7 8 9
int main( )
{
Inicializado com a string vazia.
string phrase; string adjective("fritas"), noun("formigas"); string wish = "Bon appetite!";
10 11 12
Duas formas equivalentes de inicializar uma variável string.
phrase = "Eu adoro " + noun + " " + adjective + "!"; cout << phrase << endl << wish << endl;
13 14 }
return 0;
DIÁLOGO PROGRAMA-USUÁRIO Eu adoro formigas fritas! Bom apetite!
O C++ precisa trabalhar muito para permitir que você concatene strings desse jeito simples e natural. A constante string "Eu adoro " não é um objeto de tipo string. Uma constante string como "Eu adoro " é armazenada como uma string C (em outras palavras, como um vetor de caracteres que termina com o caractere nulo). Quando o C++ vê "Eu adoro " como um argumento de +, encontra a definição (ou sobrecarga) de + que se aplica a um valor como "Eu adoro ". Existem sobrecargas do operador + que possuem uma string C do lado esquerdo e uma string do lado direito, bem como o inverso. Existe até uma versão que possui uma string C em ambos os lados do + e produz um objeto string como o valor retornado. Obviamente, existe também a sobrecarga normal, com o tipo string para ambos os operandos. O C++ não precisa, na realidade, oferecer todos esses casos de sobrecarga para +. Se essas sobrecargas não forem oferecidas, o C++ procurará um construtor que execute uma conversão de tipo para converter a string C "Eu adoro " em um valor ao qual + se aplique. Nesse caso, o construtor com o parâmetro string C executaria exatamente essa conversão. Entretanto, em geral as sobrecargas extras são consideradas mais eficientes. Muitas vezes se considera a classe string um substituto moderno para as strings C. Todavia, em C++ não se pode evitar facilmente a utilização de strings C quando se programa com a classe string. A CLASSE string A classe string pode ser utilizada para representar valores que são strings de caracteres. A classe string proporciona uma representação de strings mais versátil que as strings C de que falamos na Seção 9.1. A classe string está definida na biblioteca que também se chama , e sua definição está colocada no std namespace.
260
Strings
Os programas que utilizam a classe string devem, portanto, conter uma das seguintes instruções (ou algo mais ou menos equivalente): #include using namespace std;
ou #include using std::string;
A classe string possui um construtor-padrão que inicializa o objeto string com a string vazia, e um construtor que requer uma string C como argumento e inicializa o objeto string com um valor que representa a string fornecida como o argumento. Por exemplo: string s1, s2("Olá");
■ E/S COM A CLASSE string
Pode-se utilizar o operador de inserção >> e cout para enviar à saída objetos string exatamente como se faz com dados de outros tipos. Isso é ilustrado no Painel 9.4. A entrada com a classe string é um pouco mais complexa. O operador de extração, >>, e cin funcionam com objetos string da mesma forma que com outros dados, mas não se esqueça de que o operador de extração ignora espaços em branco iniciais e pára de ler quando encontra mais espaços em branco. Isso é verdadeiro para strings assim como para outros dados. Por exemplo, considere o seguinte código: string s1, s2; cin >> s1; cin >> s2;
Se o usuário digitar Que os pêlos dos seus dedos do pé fiquem longos e encaracolados!
então s1 receberá o valor "Que" com qualquer espaço em branco inicial (ou posterior) eliminado. A variável s2 recebe a string "os". Utilizando o operador de extração, >>, e cin, você só pode ler em palavras; não pode ler uma linha ou outra string que contenha um espaço em branco. Às vezes é exatamente isso que você quer, mas às vezes não. Se você quer que seu programa leia uma linha inteira de entrada para uma variável de tipo string, pode utilizar a função getline. A sintaxe para utilizar getline com objetos string é um pouco diferente da que descre vemos para strings C na Seção 9.1. Não use cin.getline; em vez disso, torne cin o primeiro argumento de getline.1 (Assim, essa versão de getline não é uma função-membro.) string linha; cout << "Digite uma linha de entrada:\n"; getline(cin, linha); cout << linha << "FIM DA ENTRADA\n";
Quando inserido em um programa completo, esse código produz um diálogo como o que se segue: Digite alguma coisa: La ra ra para você! La ra ra para você!FIM DA ENTRADA
Se houver espaços em branco no início ou no fim da linha, eles também farão parte do valor string lido por getline. Esta versão de getline está na biblioteca . (Como veremos no Capítulo 12, você pode usar um objeto stream conectado a um arquivo de texto em lugar de cin para efetuar a entrada a partir de um arquivo utilizando getline.) Não se pode utilizar cin e >> para ler um caractere de espaço em branco. Se você quiser ler um caractere de cada vez, pode usar cin.get, de que falamos na Seção 9.2. A função cin.get lê valores de tipo char, não de tipo 1.
Isso é um tanto irônico, já que a classe string foi projetada com técnicas de programação orientada a objetos, mais modernas, e a notação que ela utiliza para getline é bastante antiquada. Trata-se de um acidente da história. Essa função getline foi definida depois que a biblioteca iostream já estava em uso e, assim, os criadores não tiveram muita escolha além de tornar getline uma função independente.
Classe-Padrão string
261
string, mas isso pode ser útil quando se lida com a entrada de strings. O Painel 9.5 contém um programa que ilustra tanto getline quanto cin.get para a entrada de strings. A importância da função novaLinha é explicada na seção "Armadilha" intitulada "Misturando cin >> variavel; e getline". Painel 9.5
Programa utilizando a classe string //Demonstra getline e cin.get. #include #include using namespace std;
1 2 3 4
5 void newLine( ); 6 int main( ) 7 { 8 string firstName, lastName, recordName; 9 string motto = "Os seus registros são os nossos registros."; 10 cout << "Digite o seu primeiro nome e o último:\n"; 11 cin >> firstName >> lastName; 12 newLine( ); 13 14 15
recordName = lastName + ", " + firstName; cout << "O seu nome em nossos registros é: "; cout << recordName << endl;
16 17 18 19 20 21
cout << "Nosso slogan é\n" << motto << endl; cout << "Por favor, sugira um slogan melhor (de uma linha):\n"; getline(cin, motto); cout << "Nosso novo slogan será:\n"; cout << motto << endl;
22 23 24 25 26 27 28 29 30 31 32
return 0;
} //Utiliza iostream: void newLine( ) { char nextChar; do
{ cin.get(nextChar); } while (nextChar != ’\n’); }
DIÁLOGO PROGRAMA-USUÁRIO Digite o seu primeiro nome e o último: B’Elanna Torres
O seu nome em nossos registros é: Torres, B’Elanna Nosso slogan é Os seus registros são os nossos registros. Por favor, sugira um slogan melhor (de uma linha): Nossos registros vão aonde nenhum outro jamais esteve.
Nosso novo slogan será: Nossos registros vão aonde nenhum outro jamais esteve.
E/S COM OBJETOS string Pode-se utilizar o operador de inserção << com cout para enviar à saída objetos string. Pode-se ler uma string à entrada com o operador de extração >> e cin. Quando se usa >> para a entrada, o código lê uma string delimitada com espaços em branco. Pode-se utilizar a função getline para ler uma linha inteira de texto para um objeto string.
262
Strings
EXEMPLOS string saudacao("Olá"), resposta, proximaLinha; cout << saudacao; cin >> resposta; getline(cin, proximaLinha);
22. Considere o código seguinte (suponha que esteja inserido em um programa completo e correto e o execute): string s1, s2; cout << "Digite uma linha de entrada:\n"; cin >> s1 >> s2; cout << s1 << "*" << s2 << "
Se o diálogo se iniciar da seguinte forma, qual será a próxima linha de saída? Digite uma linha de entrada: A vida é bela!
23. Considere o código seguinte (suponha que esteja inserido em um programa completo e correto e o execute): string s; cout << "Digite uma linha de entrada:\n"; getline(cin, s); cout << s << "
Se o diálogo se iniciar da seguinte forma, qual será a próxima linha de saída? Digite uma linha de entrada: A vida é bela!
MAIS VERSÕES DE getline Até agora, descrevemos as seguintes formas de utilizar getline: string linha; cout << "Digite uma linha de entrada:\n"; getline(cin, linha);
Esta versão pára de ler quando encontra o sinalizador de final de linha, ’\n’. Há uma versão que permite especificar um caractere diferente para utilizar como sinal de parada. Eis um exemplo em que a parada acontece quando o primeiro ponto de interrogação é encontrado: string linha; cout << "Digite alguma coisa:\n"; getline(cin, linha, ’?’);
Faz sentido utilizar getline como se fosse uma função void, mas na realidade ela retorna uma referência para seu primeiro argumento, que é cin no código acima. Assim, o seguinte código lerá uma linha de texto para s1 e uma string de caracteres não em branco para s2: string s1, s2; getline(cin, s1) >> s2;
A invocação getline(cin, s1) retorna uma referência para cin, de modo que, depois da invocação de getline, a próxima coisa a acontecer é equivalente a cin >> s2;
Esse tipo de uso de getline parece ter sido projetado para se utilizar em um "show do milhão" de C++ e não para atender a alguma verdadeira necessidade de programação, mas às vezes se mostra útil.
MISTURANDO cin >> variavel; E getline Tome cuidado ao misturar entradas utilizando cin>>variavel; com entradas utilizando getline. Por exemplo, considere o seguinte código: int n;
Classe-Padrão string
263
string linha; cin >> n; getline(cin, linha);
Quando esse código lê a seguinte entrada, você poderia esperar que o valor de n fosse fixado em 42 e que o valor de linha fosse fixado em um valor string representando "Olá, mochileiro.": 42 Olá, mochileiro.
Entretanto, embora n realmente receba o valor de 42, linha é fixado como igual à string vazia. O que aconteceu? Com a utilização de cin >> n o espaço em branco anterior à palavra é ignorado na entrada, mas o resto da linha permanece, nesse caso apenas ’\n’, para a próxima entrada. Um comando como cin >> n;
sempre deixa algo na linha para uma getline seguinte ler (mesmo que seja apenas o ’\n’). Nesse caso, getline vê o ’\n’ e pára de ler, então getline lê uma string vazia. Se você achar que seu programa parece estar ignorando misteriosamente dados de entrada, verifique se não misturou esses dois tipos de entrada. Você pode precisar utilizar uma função novaLinha do Painel 9.5 ou a função ignore da biblioteca iostream. Por exemplo, cin.ignore(1000, ’\n’);
Com esses argumentos, uma chamada à função-membro ignore lerá e descartará todo o resto da linha até e inclusive o ’\n’ (ou até descartar 1000 caracteres, se não encontrar o final da linha depois de 1000 caracteres). Outros problemas embaraçosos podem surgir com programas que utilizam cin com >> e getline ao mesmo tempo. Além disso, esses problemas podem surgir e desaparecer quando se passa de um compilador C++ a outro. Se tudo o mais falhar, ou se você quiser ter certeza da portabilidade, pode recorrer à entrada caractere a caractere, por meio de cin.get. Esses problemas podem ocorrer com qualquer das versões de getline de que falamos neste capítulo.
getline
PARA
OBJETOS DA CLASSE string
A função getline para objetos string possui duas versões: istream& getline(istream& ins, string& strVar, char delimiter); e istream& getline(istream& ins, string& strVar);
A primeira versão desta função lê caracteres do objeto istream dados como primeiro argumento (sempre cin neste capítulo), inserindo os caracteres na variável string strVar até um caractere delimiter (delimitador) ser encontrado. O caractere delimiter é retirado da entrada e descartado. A segunda versão utiliza ’\n’ como valor-padrão do delimiter; caso contrário, funciona da mesma forma. Essas funções getline retornam seu primeiro argumento (sempre cin neste capítulo), mas normalmente são usadas como se fossem funções void.
■ PROCESSAMENTO DE STRINGS COM A CLASSE string
A classe string permite que você execute as mesmas operações, discutidas na Seção 9.1, que pode executar com as strings C e outras mais. (Muitas mais! Há mais de 100 membros e outras funções associadas com a classe string padrão.) Pode-se ter acesso aos caracteres em um objeto string da mesma forma que a elementos de vetor, e por isso os objetos string têm as vantagens dos vetores de caracteres mais diversas vantagens que os vetores não têm, como a de aumentar automaticamente sua capacidade. Se ultimoNome é o nome de um objeto string, então ultimoNome[i] dá acesso ao iésimo caractere na string representada por ultimoNome. Esse uso dos colchetes de vetor é ilustrado no Painel 9.6. O Painel 9.6 também ilustra a função-membro comprimento. Cada objeto string possui uma função-membro chamada comprimento que não requer argumentos e retorna o comprimento da string representada pelo objeto string. Assim, um objeto string não só pode ser utilizado como um vetor, mas a função-membro comprimento a faz se comportar como um vetor parcialmente preenchido que controla automaticamente quantas posições foram ocupadas.
264
Strings
Painel 9.6
1 2 3 4 5 6 7
Um objeto string pode se comportar como um vetor //Demonstra a utilização de um objeto string como se fosse um vetor. #include #include using namespace std; int main( )
{ string firstName, lastName;
8 9
cout << "Digite o seu primeiro nome e o último:\n"; cin >> firstName >> lastName;
10 11 12 13 14 15 16 17 18 19 20
cout << "O seu último nome é soletrado:\n"; int i; for (i = 0; i < lastName.length( ); i++) { cout << lastName[i] << " "; lastName[i] = ’-’; } cout << endl; for (i = 0; i < lastName.length( ); i++) cout << lastName[i] << " "; //Coloca um "-" embaixo de cada letra. cout << endl;
21 22 23 }
return 0;
cout << "Bom dia " << firstName << endl;
DIÁLOGO PROGRAMA-USUÁRIO Digite o seu primeiro nome e o último: John Crichton
O seu último nome é soletrado: C r i c h t o n - - - - - - - Bom dia, John
Os colchetes do vetor, quando usados com um objeto da classe string, não verificam os índices ilegais. Se você utilizar um índice ilegal (ou seja, um índice maior que o comprimento da string no objeto, ou igual a ele), os resultados são imprevisíveis, mas tendem a ser ruins. Pode ser que haja um comportamento estranho sem nenhuma mensagem de erro que lhe diga que o problema é um valor ilegal de índice. Existe uma função-membro chamada at que verifica se o valor do índice é ilegal. A função-membro chamada at se comporta basicamente da mesma forma que os colchetes, a não ser por duas questões: utiliza-se notação de função com at, então, em vez de a[i], utiliza-se a.at(i), e a função-membro verifica se i é avaliado como um índice ilegal. Se o valor de i em a.at(i) for um índice ilegal, você deve receber uma mensagem de erro de execução que lhe diz o que está errado. No seguinte fragmento de código, o acesso tentado está fora do intervalo; mesmo assim, não deve produzir uma mensagem de erro, embora dê acesso a uma variável indexada não-existente: string str("Rita"); cout << str[6] << endl;
O próximo exemplo, todavia, fará com que o programa se encerre de maneira anormal, de modo que você, pelo menos, saberá que há algo errado:
Classe-Padrão string
265
string str("Rita"); cout << str.at(6) << endl;
Mas esteja prevenido: alguns sistemas fornecem mensagens de erro muito pobres quando a.at(i) possui um índice ilegal i. Você pode alterar um único caractere na string atribuindo um valor char à variável indexada, como str[i]. Como a função-membro at retorna uma referência, isso também pode ser feito com a função-membro at. Por exemplo, para mudar o terceiro caractere no objeto string str para ’X’, você pode usar um dos seguintes fragmentos de código: str.at(2)=’X’; ou str[2]=’X’;
Como em um vetor de caracteres comum, as posições de caracteres para objetos de tipo string são indexadas a partir do 0, de modo que o terceiro caractere em uma string fique na posição de índice 2. O Painel 9.7 fornece uma lista parcial das funções-membros da classe string. Os objetos da classe string muitas vezes se comportam melhor que as strings C que analisamos na Seção 9.1. Em particular, o operador == sobre objetos da classe string retorna um resultado que corresponde à nossa noção intuitiva de strings iguais; ou seja, retorna true se as duas strings contêm os mesmos caracteres na mesma ordem, caso contrário, retorna false. De forma similar, os operadores de comparação <, >, <= e >= comparam objetos string utilizando ordenamento lexicográfico. (O ordenamento lexicográfico é o ordenamento alfabético que utiliza a ordem de símbolos fornecida na lista de caracteres ASCII do Apêndice 3. Se as strings são formadas só de letras maiúsculas ou só de letras minúsculas, o ordenamento lexicográfico corresponde à ordem alfabética normal.) Painel 9.7
Funções-membros da classe-padrão string (parte1 de 2)
EXEMPLO
OBSERVAÇÕES
Construtores string str;
Construtor-padrão; cria objeto string vazio str.
string str("string");
Cria um objeto string com dados "string".
string str("umaString");
Cria um objeto string que é uma cópia de umaString. umaString é um objeto da classe string.
Acesso a elemento str[i]
Fornece referência de leitura/escrita para um caractere em str no índice i.
str.at(i)
Fornece referência de leitura/escrita para um caractere em str no índice i.
str.substr(posicao, comprimento)
Fornece a substring do objeto que faz a chamada iniciando em posicao e com comprimento caracteres.
Atribuição/Modificadores str1 = str2;
Aloca espaço e o inicializa com str2 dados, libera a memória alocada para str1 e fixa o tamanho de str1 como o mesmo de str2.
str1 += str2;
Os dados de caracteres de str2 são concatenados ao final de str1; o tamanho é fixado adequadamente.
str.empty( )
Fornece true se str for uma string vazia e false caso contrário.
str1 + str2;
Fornece uma string que possui str2 dados concatenados ao final de str1 dados. O tamanho é fixado adequadamente.
str.insert(pos, str2)
Insere str2 em str, iniciando na posicao pos.
str.remove(pos, comprimento)
Remove substring de tamanho comprimento, iniciando na posição pos.
Comparações str1 == str2 str1!= str2
Compara para verificar a igualdade ou a desigualdade; fornece um valor booleano.
str1 < str2 str1 > str2
Quatro comparações. Todas são comparações lexicográficas.
str1 <= str2 str1 >= str2 str.find(str1)
Fornece índice da primeira ocorrência de str1 em str.
str.find(str1, pos)
Fornece índice da primeira ocorrência da string str1 em str; a busca inicia na posição pos.
266
Strings
Painel 9.7
Funções-membros da classe-padrão string (parte 2 de 2)
str.find_primeiro_em(str1, pos)
Fornece índice da primeira instância em str de qualquer caractere em str1, iniciando a busca na posição pos.
str.find_primeiro_not_em(str1, pos)
Fornece índice da primeira instância em str de qualquer caractere que em str1, iniciando a busca na posição pos.
não se
encontra
= E == SÃO DIFERENTES PARA strings E STRINGS C Os operadores =, ==, !=, <, >, <= e >=, quando utilizados com o tipo string padrão de C++, produzem resultados que correspondem à nossa noção intuitiva de como as strings devem ser comparadas. Não se comportam mal como com as strings C, de que tratamos na Seção 9.1.
TESTE DO PALÍNDROMO Um palíndromo é uma string lida da mesma forma de trás para a frente e de frente para trás. O programa no Painel 9.8 testa uma string de entrada para verificar se se trata de um palíndromo. Nosso teste do palíndromo ignora todos os espaços e pontuação e considera as versões em maiúscula e minúscula de uma letra como iguais quando se trata de decidir se algo é um palíndromo. Alguns exemplos de palíndromos: asa arara radar sopapos A breve verba Morram após a sopa marrom A base desatola calotas e desaba Socorram-me, subi no ônibus em Marrocos A função removePontuacao é interessante, pois utiliza as funções-membros strings substr e find. A função-membro substr extrai uma substring do objeto que faz a chamada, dados a posição e o comprimento da substring desejada. As primeiras três linhas de removePontuacao declaram variáveis para uso na função. O loop for percorre os caracteres do parâmetro s um de cada vez e tenta encontrá-los na string pontuacao . Para fazer isso, uma string que é a substring de s, de comprimento 1 em cada posição de caractere, é extraída. A posição dessa substring em pontuacao é determinada por meio da função-membro find. Se essa string de um caractere não é a string pontuacao , a string de um caractere é concatenada à string naoPontuacao que deve ser fornecida.
Painel 9.8
1 2 3 4 5
Programa teste de palíndromo ( parte 1 de 3) //Testa palíndromo adequadamente. #include #include #include using namespace std;
6 void swap(char& v1, char& v2); 7 //Troca os valores de v1 e v2. 8 string reverse(const string& s); 9 //Retorna uma cópia de s, mas com os caracteres na ordem inversa. 10 string removePunct(const string& s, const string& punct); 11 //Retorna uma cópia de s com todas as ocorrências dos caracteres 12 //na string punct removidos. 13 string makeLower (const string& s); 14 //Retorna uma cópia de s com todos os caracteres em maiúscula 15 //passados para minúscula, com os outros caracteres inalterados. 16 bool isPal(const string& s); 17 //Retorna true se s for um palíndromo; caso contrário, retorna false.
Classe-Padrão
Painel 9.8
Programa teste de palíndromo ( parte 2 de 3)
18 int main( ) 19 { 20 string str; 21 cout << "Digite um candidato para o teste do palíndromo\n" 22 << "e em seguida pressione a tecla Enter.\n"; 23 getline(cin, str); 24 25 26 27 28
if (isPal(str))
29 30 }
return 0;
cout << "\"" << str + "\" é um palíndromo."; else
cout << "\"" << str + "\" não é um palíndromo."; cout << endl;
31 32 void swap(char& v1, 33 { 34 char temp = v1; 35 v1 = v2; 36 v2 = temp; 37 }
char&
v2)
38 string reverse(const string& s) 39 { 40 int start = 0; 41 int end = s.length( ); 42 string temp(s); 43 44 45 46 47 48
while (start
end--; swap(temp[start], temp[end]); start++; }
49 50 }
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
< end)
{
return temp;
//Utiliza e string makeLower(const string& s) { string temp(s); for (int i = 0; i < s.length( ); i++) temp[i] = tolower(s[i]); return temp;
} string removePunct(const string& s, const string& punct) { string noPunct; //inicializado para a string vazia int sLength = s.length( ); int punctLength = punct.length( ); for (int i = 0; i < sLength; i++)
string
267
268
Strings
Painel 9.8
67 68 69 70 71
Programa teste de palíndromo ( parte 3 de 3)
{ string aChar = s.substr(i,1); //String de um caractere int location = punct.find(aChar, 0);
//Encontra a localização de caracteres sucessivos //de src em punct.
72 73 74 75 76 77 }
if (location < 0 || location >= punctLength)
noPunct = noPunct + aChar; //aChar não está em punct, então prossegue } return noPunct;
78 //Utiliza as funções makeLower, removePunct 79 bool isPal(const string& s) 80 { 81 string punct(",;:.?!’\" "); //inclui um espaço em branco 82 string str(s); 83 str = makeLower(str); 84 string lowerStr = removePunct(str, punct); 85 86 }
return (lowerStr == reverse(lowerStr));
DIÁLOGOS PROGRAMA-USUÁRIO Digite um candidato para o teste do palíndromo e em seguida pressione a tecla Enter. A breve verba . “A breve verba” é um palíndromo.
Digite um candidato para o teste do palíndromo e em seguida pressione a tecla Enter. Radar
"Radar" é um palíndromo.
Digite um candidato para o teste do palíndromo e em seguida pressione a tecla Enter. Eu sou um palíndromo ? "Eu sou um palíndromo?" não é um palíndromo.
24. Considere o seguinte código: string s1, s2("Olá"); cout << "Digite uma linha de entrada:\n"; cin >> s1; if (s1 == s2) cout << "Igual\n"; else cout << "Não igual\n";
Classe-Padrão string
269
Se o diálogo se iniciar da seguinte forma, qual será a próxima linha de saída? Digite uma linha de entrada: Olá amigo!
25. Qual é a saída produzida pelo seguinte código? string s1, s2("Oi"); s1 = s2; s2[0] = ’U’; cout << s1 << " " << s2;
CONVERSÃO ENTRE OBJETOS string E C-STRINGS Você já sabe que o C++ efetuará uma conversão de tipo automática para permitir o armazenamento de uma string C em uma variável de tipo string. Por exemplo, o seguinte código funcionará bem: ■
char umaStringC[] = "Esta é a minha string C.";
string variavelString; variavelString = umaStringC;
O seguinte, entretanto, produzirá uma mensagem de erro do compilador: umaStringC = variavelString; //ILEGAL
O seguinte também é ilegal: strcpy(umaStringC, variavelString); //ILEGAL
strcpy não pode tomar um objeto string como segundo argumento e não há conversão de tipo automática de objetos string em strings C, um problema do qual parece difícil nos livrarmos. Para obter a string C correspondente a um objeto string é preciso efetuar uma conversão explícita. Isto pode ser feito com a função-membro string c_str( ). A versão correta da cópia que estávamos tentando fazer é a seguinte: strcpy(umaStringC, variavelString.c_str( )); //Legal;
Observe que você não precisa utilizar a função strcpy para efetuar a cópia. A função-membro c_str( ) retorna a string C que corresponde ao objeto que chama a string. Como já observamos neste capítulo, o operador de atribuição não funciona sem strings C. Assim, caso você ache que o código seguinte funcionará, gostaríamos de lhe avisar que este também é ilegal. umaStringC = variavelString.c_str( ); //ILEGAL
■
■
■ ■
■
Uma variável string C é o mesmo que um vetor de caracteres, mas é utilizada de uma forma levemente diferente. Uma variável string emprega o caractere nulo, ’ \0’, para assinalar o fim da string armazenada no vetor. Variáveis string C normalmente devem ser tratadas como vetores e não como simples variáveis do tipo que utilizamos para números e caracteres únicos. Em particular, não se pode atribuir um valor string C a uma variável string C utilizando o sinal de igual, =, e não se podem comparar os valores em duas variáveis string C com o operador ==. Em vez disso, você deve utilizar funções string C especiais para executar essas tarefas. A biblioteca possui diversas funções úteis de manipulação de caracteres. Pode-se utilizar cin.get para ler um único caractere de entrada sem ignorar espaços em branco. A função cin.get lê o próximo caractere independentemente de que tipo este seja. Várias versões da função getline podem ser usadas para ler uma linha inteira de entrada a partir do teclado.
270
Strings
■
■
A biblioteca do padrão ANSI/ISO oferece uma classe plenamente desenvolvida chamada string que pode ser utilizada para representar strings de caracteres. Objetos da classe string se comportam melhor do que strings C. Em particular, os operadores de atribuição e de igualdade, = e ==, apresentam o significado intuitivo quando utilizados como objetos da classe string.
RESPOSTAS DOS EXERCÍCIOS DE AUTOTESTE 1. As duas declarações seguintes são equivalentes (mas não equivalentes a outras): char varString[10] char varString[10]
= "Olá"; = {’O’, ’l’, ’á’, ’\0’};
As duas declarações seguintes são equivalentes (mas não equivalentes a outras): char varString[6] char varString[]
= "Olá"; = "Olá";
A declaração seguinte não é equivalente a nenhuma das outras: char varString[10]
= {’O’, ’l’, ’á’};
2. "LaRaRa para você" 3. A declaração significa que varString tem espaço para apenas seis caracteres (inclusive o caractere nulo, ’\0’). A função strcat não verifica se existe espaço para acrescentar mais caracteres a varString, por isso strcat escreverá todos os caracteres da string " e Tchau." na memória, mesmo que isso exija mais memória do que tenha sido atribuída a varString. Isso significa que memória, que não deveria ser alterada, será. O efeito é imprevisível e, sem dúvida, ruim. 4. Se strlen já não houvesse sido definida para você, seria possível utilizar a seguinte definição de função: int strlen(const char str[])
//Pré-condição: str contém um valor string terminado //por ’\0’. //Retorna o número de caracteres na string str (sem //contar o ’\0’). { int indice = 0; while (str[indice] != ’\0’) indice++; return indice; }
5. O número máximo de caracteres é cinco porque a sexta posição é necessária para o caractere nulo (’ \0’). 6. a. 1 b. 1 c. 5 (incluindo o ’\0’) d. 2 (incluindo o ’\0’) e. 6 (incluindo o ’\0’) 7. Não são equivalentes. A primeira linha coloca o caractere nulo ’ \0’ no vetor depois dos caracteres ’ a’, ’b’ e ’c’. A segunda apenas atribui as posições sucessivas ’ a’, ’b’ e ’c’, mas não colocam o ’ \0’ em nenhum lugar. 8. int indice = 0; while (
nossaString[indice] != ’\0’ )
{ }
nossaString[indice] = ’X’; indice++;
9. a. Se a variável string C não possui um terminador nulo, ’\0’, o loop pode ser executado além da memória alocada para a string C, destruindo o conteúdo da memória nesse local. Para proteger a memória além do final do vetor, altere a condição de while, como mostrado em b. b. while( nossaString[indice] != ’\0’ && indice < TAMANHO ) 10. #include //necessário para obter a declaração de strcpy ...
Respostas dos Exercícios de Autoteste
271
strcpy(umaString, "Olá"); I did it my way!
11. 12. A string "Bem, eu acho." é longa demais para umaString. Um bloco de memória que não pertence ao vetor umaString será apagado. 13. O diálogo completo é assim: Digite alguma coisa: A hora é agora. A-hora
14. O diálogo completo é assim: Digite uma linha de entrada: Que os pêlos dos seus dedos do pé fiquem longos e encaracolados. Que o
15. O diálogo completo é assim: Digite uma linha de entrada: a b c d e f g a b FIM DA ENTRADA
16. O diálogo completo é assim: Digite uma linha de entrada: abcdef gh ace h
Observe que a saída apresenta o padrão "um caractere de entrada sim, outro não" e que o espaço em branco é tratado como qualquer outro caractere. 17. O diálogo completo é assim: Digite uma linha de entrada: 0 1 2 3 4 5 6 7 8 9 10 11 01234567891 1
Observe que apenas o ’ 1’ da string de entrada 10 é apresentado na saída. Isso acontece porque cin.get está lendo caracteres, não números e, assim, lê a entrada 10 como os dois caracteres, ’ 1’ e ’0’. Como esse código foi escrito para ecoar um caractere sim, outro não, o ’ 0’ não aparece na saída. Dessa forma, o próximo caractere, que é um espaço em branco, aparece na saída. De forma similar, apenas um dos dois caracteres ’1’ de 11 aparece na saída. Se isso não estiver claro, escreva a saída em uma folha de papel e utilize um quadradinho no lugar do caractere em branco. Então, faça uma cruz nos caracteres alternadamente (um sim, outro não); a saída mostrada acima é o resultado. 18. Esse código contém um loop infinito e prosseguirá enquanto o usuário continuar a fornecer entradas. A expressão booleana (proximo != ’\n’) sempre será true, porque proximo é preenchido por meio do comando cin >> proximo;
e esse comando sempre ignora o caractere de nova linha character, ’ \n’ (assim como qualquer outro espaço em branco). O código será executado e, se o usuário não fornecer mais dados de entrada, o diálogo será o seguinte: Digite uma linha de entrada: 0 1 2 3 4 5 6 7 8 9 10 11 0246811
Esse código envia para a saída os caracteres não-brancos alternadamente (um sim, outro não). Os dois caracteres ’1’ na saída são o primeiro caractere na entrada 10 e o primeiro caractere na entrada 11. 19. O diálogo completo é assim:
20.
Digite uma linha de entrada: Vejo você às 10h30. Vejo você às 1
272
Strings
cin.get(proximo); if (!isupper(proximo))
cout << proximo; } while (proximo != ’\n’);
Observe que você deveria usar !isupper(proximo) e não islower(proximo). Isso porque islower(proximo) retorna false se proximo contiver um caractere que não seja uma letra (como o espaço em branco ou o símbolo da vírgula). 21. //Utiliza iostream: void novaLinha( )
{ cin.ignore(10000, ’\n’); }
Obviamente, isso só funciona para linhas com menos de 10.000 caracteres, mas qualquer linha maior que isso provavelmente indicará algum outro problema não relacionado a este. 22. A*vida
Igual 25. Oi Ui
PROJETOS DE PROGRAMAÇÃO 1. Escreva um programa que leia uma sentença de até 100 caracteres. Na sentença de saída dois ou mais espaços em branco deverão ser comprimidos em apenas um espaço em branco. Além disso, a sentença deve começar com uma letra maiúscula, mas não deve conter outras letras maiúsculas. Não se preocupe com os nomes próprios; se sua primeira letra for alterada para minúscula, não há problema. Trate uma quebra de linha como se fosse um espaço em branco, no sentido de que uma quebra de linha e qualquer número de espaços em branco sejam comprimidos em um único espaço em branco. Presuma que a sentença termine com um ponto final e não contenha outros pontos finais. Por exemplo, a entrada a Resposta para a Vida, o Universo e Sabe Lá O Que Mais É 42.
deve produzir a seguinte saída: A resposta para a vida, o universo e sabe lá o que mais é 42.
2. Escreva um programa que leia uma linha de texto e apresente como saída o número de palavras na linha e o número do ocorrências de cada letra. Defina uma palavra como qualquer string de letras que seja delimitada em cada extremidade por um espaço em branco, um ponto final, uma vírgula ou o início ou fim da linha. Pode supor que a entrada consista inteiramente em letras, espaços em branco, vírgulas e pontos finais. Quando enviar à saída o número de letras que ocorrem em uma linha, não deixe de contar as versões maiúsculas e minúsculas de uma letra como a mesma letra. Apresente na saída as letras em ordem alfabética e liste apenas aquelas letras que ocorrem na linha de entrada. Por exemplo, a linha de entrada Eu sou dez.
deve produzir uma saída semelhante à seguinte: 3 2 2 1 1 1 1
palavras e u s o d z
Projetos de Programação
273
3. Escreva um programa que leia o nome de uma pessoa no seguinte formato: primeiro nome, depois nome do meio ou inicial e, por fim, o sobrenome. O programa apresenta como saída o nome no seguinte formato: Sobrenome, Primeiro_Nome, Inicial_Do_Meio.
Por exemplo, a entrada Maria Alegre Usuária
deve produzir a saída Usuária, Maria A.
A entrada Maria A. Usuária
também deve produzir a saída Usuária, Maria A.
Seu programa deve colocar um ponto final depois da inicial do meio, mesmo se a entrada não contiver tal ponto. Deve também permitir que os usuários não forneçam nome do meio ou inicial do meio. Nesse caso, é óbvio que a saída não contém nome do meio ou inicial. Por exemplo, a entrada Maria Usuária
deve produzir a saída Usuária, Maria
Se você utilizar strings C, suponha que cada nome tenha no máximo 20 caracteres de comprimento. Uma outra opção é utilizar a classe string. (Dica: você pode querer utilizar três variáveis string em vez de uma grande variável string para a entrada. Pode achar mais fácil não usar getline.) 4. Escreva um programa que leia uma linha de texto e substitua todas as palavras de quatro letras pela pala vra "amor". Por exemplo, a string de entrada Silvio é um bobo!
deve produzir a saída: Silvio é um amor!
Claro que a saída nem sempre fará sentido. Por exemplo, a string de entrada João vai correr para casa.
deve produzir a saída: Amor vai correr amor amor.
Se a palavra de quatro letras começar com uma letra maiúscula, deve ser substituída por "Amor", não por "amor". Não é preciso verificar as letras maiúsculas, só na primeira letra de uma palavra. Uma palavra é uma string formada pelas letras do alfabeto e delimitada em cada extremidade por um espaço em branco, pelo final da linha ou qualquer outro caractere que não seja uma letra. Seu programa deve repetir essa ação até o usuário dizer que deseja sair. 5. Escreva um programa que possa ser usado para treinar o usuário a utilizar uma linguagem menos machista, sugerindo versões alternativas de sentenças dadas pelo usuário. O programa pedirá uma sentença, lerá a sentença para uma variável string e substituirá todas as ocorrências de pronomes masculinos com pronomes de gênero neutro. Por exemplo, substituirá "ele" por "ela ou ele". Assim, a sentença de entrada Consulte um orientador, fale com ele e escute o que ele disser.
deve produzir a seguinte versão alterada: Consulte um orientador, fale com ela ou ele e escute o que ela ou ele disser.
Não deixe de preservar as letras maiúsculas na primeira palavra da sentença. O pronome "dele" pode ser substituído por "dela ou dele"; não é necessário que seu programa decida entre "dele" ou "delas". Permita que o usuário repita o processo para mais sentenças até dizer que deseja encerrar o programa. Será um longo programa, que exigirá uma boa dose de paciência. Seu programa não deve substituir a string "ele" quando ocorrer dentro de outra palavra, como "elemento". Uma palavra é qualquer string formada por letras e delimitada em cada extremidade por um espaço em branco, o final da linha ou qualquer outro caractere que não seja uma letra. Permita que suas sentenças tenham até 100 caracteres de extensão. 6. Um CD à venda contém imagens .jpeg e .gif de músicas sob domínio público. O CD inclui um arqui vo formado por linhas contendo os títulos, depois os compositores, um por linha. O nome da música vem primeiro, depois zero ou mais espaços, depois um caractere de separação (-), depois um ou mais espaços, depois o nome do compositor. O nome do compositor pode ser apenas o sobrenome, ou uma inicial e o sobrenome ou dois nomes (primeiro — último) ou três nomes (primeiro — do meio — último). Existem
274
Strings
algumas canções em que consta "nenhum autor listado" como nome do autor. No processamento subseqüente, "nenhum autor listado" não deve ser rearranjado. Eis uma lista bem resumida dos títulos e autores. 1. Adagio Sonata "Ao luar" - Ludwig Van Beethoven 2. An Alexis - F.H. Hummel and J.N. Hummel 3. A La Bien Aimee - Ben Schutt 4. At Sunset - E. MacDowell 5. Angelus - J. Massenet 6. Dança de Anitra - Edward Grieg 7. Morte de Ase - Edward Grieg 8. Au Matin- Benj. - Godard ... 37. The Dying Poet - L. Gottschalk 38. Marcha Fúnebre - G.F. Handel 39. Do They Think of Me At Home - Chas. W. Glover 40. The Dearest Spot - W.T. Wrighton 1. Evening - L. Van Beethoven 2. Embarrassment - Franz Abt 3. Erin is my Home - nenhum autor listado 4. Ellen Bayne - Stephen C. Foster ... 9. Alla Mazurka - A. Nemerowsky ... 1. The Dying Volunteer - A.E. Muse 2. Dolly Day - Stephen C. Foster 3. Dolcy Jones - Stephen C. Foster 4. Dickory, Dickory, Dock - nenhum autor listado 5. The Dear Little Shamrock - nenhum autor listado 6. Dutch Warbler - nenhum autor listado ... A tarefa final é produzir uma lista alfabética de compositores seguida por uma lista de suas músicas em ordem alfabética por título dentro de cada compositor. Este exercício fica mais fácil se for dividido em partes: Escreva código para: a. Remover números iniciais, pontos finais e qualquer espaço de modo que a primeira palavra do título seja a primeira palavra da linha. b. Substituir todos os espaços múltiplos por um único espaço. c. Alguns títulos podem ter vários caracteres, por exemplo: 20. Ba- Be- Bi- Bo- Bu - nenhum autor listado Substitua todos os hífens - em qualquer linha antes do final da linha por um espaço, a não ser o último. d. Talvez a última palavra no título tenha o caractere - sem nenhum espaço entre ele e o caractere =. Inclua o espaço. e. Quando colocar o título em ordem alfabética, não considere uma inicial "A", "O", "Um" ou "Uma" no título. Escreva código para deslocar essas palavras iniciais para logo antes do caractere -. Uma vírgula depois da última palavra no título não é obrigatória, mas seria interessante. Isso pode ser feito depois que os nomes dos compositores forem deslocados para a frente, mas obviamente o código será diferente. f. Desloque os nomes dos compositores para o início da linha, seguidos pelo caractere -, seguido pelo título da composição. g. Desloque qualquer primeira inicial, primeiros e segundos nomes dos compositores para depois do sobrenome do compositor. Se o compositor for "nenhum autor listado", essa string não deve ser rearran jada. Portanto, teste essa combinação.
Projetos de Programação
275
h. Coloque em ordem alfabética, por compositor, utilizando qualquer rotina que conheça. Pode ignorar qualquer duplicata do sobrenome do compositor, como CPE Bach e JS Bach, mas colocar em ordem o segundo nome do compositor seria interessante. Pode-se utilizar a ordenação por inserção, por seleção, bubble sort ou outro algoritmo de ordenação. i. Caso não tenha feito isso ainda, desloque aqueles "A", "O", "Um" ou "Uma" que iniciam alguns títulos para o final do título. Então, dentro de cada compositor, coloque os títulos das composições em ordem alfabética. j. Guarde uma cópia do seu projeto e código. Mais tarde, pediremos para você fazer isso novamente utilizando o STL vector container.
Página em branco
Ponteiros e Vetores Dinâmicos Ponteiros e Vetores Dinâmicos
Capítulo 10Ponteiros e Vetores Dinâmicos A memória é necessária para todas as operações da razão. Blaise Pascal, Pensamentos
INTRODUÇÃO Um ponteiro é um elemento que nos dá maior controle sobre a memória do computador. Este capítulo mostrará como os ponteiros são usados com vetores e apresentará uma nova forma de vetor que se chama vetor dinamicamente alocado . Os vetores dinamicamente alocados (vetores dinâmicos, para abreviar) são vetores cujo tamanho é determinado enquanto o programa é executado, em vez de ser fixados quando o programa é escrito. Antes de ler as Seções 10.1 e 10.2 sobre ponteiros e vetores dinamicamente alocados, leia do Capítulo 1 ao 6 (omitindo as seções sobre vectors, se desejar). Não precisa ler do Capítulo 7 ao 9. Pode até mesmo ler as Seções 10.1 e 10.2 depois de ler apenas os Capítulos de 1 a 5, desde que ignore as poucas passagens que mencionam classes. A Seção 10.3 trata de algumas ferramentas para classes que só adquirem relevância assim que se começa a utilizar ponteiros e dados dinamicamente alocados (como vetores dinamicamente alocados). Antes de ler a Seção 10.3, leia os Capítulos de 1 a 8, omitindo, se desejar, as partes que tratam de vectors. Você pode ler este capítulo, o Capítulo 11 (sobre compilação e namespaces), o Capítulo 12 (sobre arquivos de E/S) e o Capítulo 13 (sobre recursão) em qualquer ordem. Se você não ler as seções sobre namespaces do Capítulo 11 antes deste capítulo, talvez ache melhor revisar a seção do Capítulo 1 intitulada "Namespaces". 10.1
Ponteiros Por vias indiretas descobrem-se as direções. William Shakespeare, Hamlet
Um ponteiro é o endereço de memória de uma variável. Como dissemos no Capítulo 5, a memória do computador é dividida em posições numeradas (chamadas bytes ), e essas variáveis são implementadas como uma seqüência de posições de memória adjacentes. Dissemos, também, que às vezes o sistema C++ utiliza esses endereços de memória como nomes para as variáveis. Se uma variável é implementada como, digamos, três posições de memória, o endereço da primeira dessas posições de memória algumas vezes é empregado como nome dessa variável. Por exemplo, quando a variável é utilizada por um argumento chamado por referência, esse endereço, não o nome da variável identificadora, é transmitido para a função que faz a chamada. Um endereço utilizado para nomear uma variável dessa forma (dando o endereço da memória onde a variável começa) é chamado de pon- teiro , porque o endereço "aponta" para a variável, identificando-a ao informar onde ela está, e não informando qual é o seu nome.
278
Ponteiros e Vetores Dinâmicos
Você já vinha utilizando ponteiros em diversas situações. Como observamos no parágrafo anterior, quando uma variável é um argumento chamado por referência em uma chamada de função, a função é dada na variável do argumento na forma de um ponteiro para a variável. Como foi comentado no Capítulo 5, um vetor é dado a uma função (ou a qualquer outra coisa), fornecendo-se um ponteiro para o primeiro elemento do vetor. (Àquela altura, chamamos esses ponteiros de "endereços de memória", mas isso é o mesmo que ponteiro.) Os ponteiros apresentam duas utilidades poderosas, mas o sistema C++ lida com eles automaticamente. Este capítulo vai lhe mostrar como escrever programas que manipulem diretamente ponteiros em vez de confiar no sistema para fazer isso por você. ■
VARIÁVEIS PONTEIROS
Um ponteiro pode ser armazenado em uma variável. Entretanto, ainda que um ponteiro seja um endereço de memória e um endereço de memória seja um número, não se pode armazenar um ponteiro em uma variável de tipo int ou double. Uma variável para guardar um ponteiro deve ser declarada como de tipo ponteiro. Por exemplo, a linha seguinte declara p como uma variável ponteiro que pode guardar um ponteiro que aponta para uma variável de tipo double: double *p;
A variável p pode guardar ponteiros para variáveis de tipo double, mas normalmente não pode conter um ponteiro para uma variável de algum outro tipo, como int ou char. Cada tipo de variável requer um tipo de ponteiro diferente.1 Em geral, para declarar uma variável que pode guardar ponteiros para outras variáveis de um tipo específico, declara-se a variável ponteiro da mesma forma que se declararia uma variável comum desse tipo, mas se coloca um asterisco diante do nome da variável. Por exemplo, a linha seguinte declara as variáveis p1 e p2 para que possam guardar ponteiros para variáveis de tipo int; declara também duas variáveis comuns v1 e v2 de tipo int: int *p1,
*p2, v1, v2;
Deve haver um asterisco diante de cada variável ponteiro. Se você omitir o segundo asterisco na declaração acima, p2 não será uma variável ponteiro; em vez disso, será uma variável comum de tipo int. DECLARAÇÕES DE VARIÁVEL PONTEIRO Uma variável que pode guardar ponteiros para outras variáveis de tipo Nome_Do_Tipo é declarada de forma semelhante àquela com que se declara uma variável de tipo Nome_Do_Tipo, exceto pelo fato de que se coloca um asterisco no início do nome da variável.
SINTAXE Nome_Do_Tipo *Nome_Da_Variavel1, *Nome_Da_Variavel2,. . .;
EXEMPLO double *ponteiro1,
*ponteiro2;
ENDEREÇOS E NÚMEROS Um ponteiro é um endereço, e um endereço é um inteiro, mas um ponteiro não é um inteiro. Isso não é loucura — é abstração! O C++ insiste em que você utilize um ponteiro como um endereço e não como um número. Um ponteiro não é um valor de tipo int ou de qualquer outro tipo numérico. Normalmente não se pode armazenar um ponteiro em uma variável de tipo int. Se você tentar, a maioria dos compiladores de C++ emitirá uma mensagem de erro ou de alerta. Além disso, não se podem executar operações aritméticas normais com ponteiros. (Como você verá mais adiante neste capítulo, podem-se executar um tipo de adição e um tipo de subtração com ponteiros, mas não são a adição e a subtração de inteiros comuns.)
1.
Existem formas de se colocar um ponteiro de um tipo em uma variável ponteiro para outro tipo, mas isso não acontece automaticamente e revela um estilo muito deselegante.
Ponteiros
279
Quando se fala de ponteiros e de variáveis ponteiros, em geral falamos de apontar e não de endereços . Quando uma variável ponteiro, como p1, contém o endereço de uma variável, como v1, diz-se que a variável ponteiro aponta para a variável v1 ou que é um ponteiro para a variável v1. Variáveis ponteiros, como p1 e p2 declaradas acima, podem conter ponteiros para variáveis como v1 e v2. Pode-se utilizar o operador & para determinar o endereço de uma variável e, então, pode-se atribuir esse endereço a uma variável ponteiro. Por exemplo, a declaração seguinte fixará a variável p1 como igual a um ponteiro que aponta para a variável v1: p1 = &v1;
Existem duas formas de se referir a v1: pode-se chamá-la de v1 ou de "a variável apontada por p1". Em C++, o modo como se diz "a variável apontada por p1" é *p1. É o mesmo asterisco que utilizamos quando declaramos p1, mas agora ele possui outro significado. Quando o asterisco é utilizado dessa forma, é chamado de operador de desreferenciação, e diz-se que a variável ponteiro foi desreferenciada . TIPOS PONTEIRO
Há uma certa inconsistência (ou, pelo menos, uma confusão em potencial) no modo como o C++ nomeia os tipos de ponteiro. Se você quiser um parâmetro cujo tipo seja, por exemplo, um ponteiro para variáveis de tipo int, então se escreve o tipo como int*, como no seguinte exemplo: void manipulaPonteiro(int* p);
Se você quiser declarar uma variável do mesmo tipo de ponteiro, o * vai com a variável, como no seguinte exemplo: int *p1, *p2;
Na realidade, o compilador não se importa se o * está ligado ao int ou ao nome da variável, então a seguinte linha também é aceita pelo compilador e possui o mesmo significado: void manipulaPonteiro(int *p);//Aceito, mas não tão elegante . int* p1, *p2;//Aceito, mas perigoso .
Entretanto, consideramos as primeiras versões mais claras. Em particular, observe que, quando se declaram variáveis, deve haver um * para cada variável ponteiro.
Juntar todas essas peças produz resultados surpreendentes. Considere o seguinte código: v1 = 0; p1 = &v1; *p1 = 42; cout << v1 << endl; cout << *p1 << endl;
Esse código apresentará o seguinte na tela: 42 42
Como p1 contém um ponteiro que aponta para v1, então v1 e *p1 se referem à mesma variável. Assim, quando se fixa *p1 como igual a 42, também se está fixando v1 como igual a 42. O símbolo &, que é usado para se obter o endereço de uma variável, é o mesmo que se usa em declarações de função para especificar um parâmetro chamado por referência. Isso não é uma coincidência. Lembre-se de que um argumento chamado por referência é implementado fornecendo-se o endereço do argumento para a função que faz a chamada. Assim, esses dois usos do símbolo & estão intimamente relacionados, embora não sejam exatamente o mesmo. OPERADORES * E &
O operador * diante de uma variável ponteiro produz a variável para a qual aponta. Quando utilizado dessa forma, o operador * é chamado de operador de desreferenciação. O operador & diante de uma variável comum produz o endereço dessa variável; ou seja, produz um ponteiro que aponta para a variável. O operador & é chamado simplesmente de operador de endereço. Por exemplo, considere as declarações double *p, v;
280
Ponteiros e Vetores Dinâmicos
A declaração seguinte fixa o valor de p de modo que p aponte para a variável v: p = &v; *p produz a variável apontada por p, de modo que, depois da atribuição apresentada, *p e v se referem à mesma variável. Por exemplo, a declaração seguinte fixa o valor de v como 9,99, embora o nome v jamais seja utilizado explicitamente: *p = 9,99;
Pode-se atribuir o valor de uma variável ponteiro a outra variável ponteiro. Por exemplo, se p1 ainda estiver apontando para v1, a linha seguinte fará com que p2 também aponte para v1: p2 = p1;
Desde que não tenhamos alterado o valor de v1, o comando seguinte também enviará 42 para a tela: cout << *p2;
Não vá confundir p1 = p2;
com *p1 = *p2;
Quando se acrescenta o asterisco, não se está lidando com os ponteiros p1 e p2, e sim com as variáveis para as quais os ponteiros estão apontando. Isso é ilustrado no Painel 10.1, no qual as variáveis são representadas como caixas e o valor da variável é escrito dentro da caixa. Não mostramos os verdadeiros endereços numéricos nas variáveis ponteiros porque os números não são importantes. O importante é que o número é o endereço de alguma variável particular. Assim, em vez de utilizar o número verdadeiro do endereço, apenas indicamos o endereço com uma seta que aponta para a variável com esse endereço. Painel 10.1
Usos do operador de atribuição com variáveis ponteiros
VARIÁVEIS PONTEIROS UTILIZADAS COM = Se p1 e p2 são variáveis ponteiros, então o comando p1 = p2;
altera o valor de p1 de modo que este seja o endereço de memória (ponteiro) em p2. Um modo comum de se pensar sobre isso é que a atribuição altera p1 para que aponte para a mesma coisa para a qual p2 está apontando no momento.
Como um ponteiro pode ser usado para se referir a uma variável, seu programa pode manipular variáveis mesmo se estas não tiverem identificadores de nomes nelas. O operador new pode ser utilizado para criar variáveis que
Ponteiros
281
não possuem identificadores para servir como seus nomes. Essas variáveis sem nome são referenciadas por meio dos ponteiros. Por exemplo, a declaração seguinte cria uma nova variável de tipo int e fixa a variável ponteiro p1 como igual ao endereço dessa nova variável (ou seja, p1 aponta para essa nova variável sem nome): p1 = new int;
É possível referir-se a essa nova variável sem nome como *p1 (ou seja, como a variável apontada por p1). Com essa variável sem nome, pode-se fazer qualquer coisa que se possa fazer com outra variável de tipo int. Por exemplo, o código seguinte lê um valor de tipo int a partir do teclado para essa variável sem nome, acrescenta 7 ao valor e depois apresenta esse novo valor como saída: cin >> *p1; *p1 = *p1 + 7; cout << *p1;
O operador new produz uma variável nova, sem nome, e retorna um ponteiro que aponta para essa nova variá vel. Especifica-se o tipo dessa nova variável escrevendo-se o nome do tipo após o operador new. As variáveis criadas utilizando-se o operador new são chamadas de variáveis alocadas dinamicamente ou simplesmente variáveis dinâmicas, porque são criadas e destruídas enquanto o programa é executado. O programa no Painel 10.2 demonstra algumas operações simples com ponteiros e variáveis dinâmicas. O Painel 10.3 ilustra graficamente a atuação do programa no Painel 10.2. OPERADOR new
O operador new cria uma nova variável dinâmica de um tipo especificado e retorna um ponteiro que aponta para essa nova variável. Por exemplo, o código seguinte cria uma nova variável dinâmica de tipo MeuTipo e faz com que a variável ponteiro p aponte para essa nova variável: MeuTipo *p; p = new MeuTipo; Se o tipo for um tipo-classe, o construtor-padrão é chamado para a variável dinâmica recém-criada. Pode-se especificar um construtor diferente incluindo argumento, da seguinte forma: MeuTipo *mtPtr; mtPtr = new MeuTipo(32.0, 17); // chama MeuTipo(double, int); Uma notação similar permite que se inicializem variáveis dinâmicas de tipos não-classe, como ilustrado a seguir: int *n; n = new int(17); // inicializa *n como 17 Em compilador de C++ antigos, se não houvesse memória suficiente disponível para criar a nova variável, new fornecia um ponteiro especial chamado NULL. O padrão C++ cuida para que, se não houver memória suficiente disponível para criar a nova variável, o operador new encerre o programa (se nada diferente for especificado).
Painel 10.2
Manipulações básicas de ponteiros ( parte 1 de 2)
1 2 3 4
//Programa para demonstrar ponteiros e variáveis dinâmicas. #include using std::cout; using std::endl;
5 6 7
int main( )
{ int *p1, *p2;
8 9 10 11 12
p1 = new int; *p1 = 42; p2 = p1; cout << "*p1 == " << *p1 << endl; cout << "*p2 == " << *p2 << endl;
13 14 15
*p2 = 53; cout << "*p1 == " << *p1 << endl; cout << "*p2 == " << *p2 << endl;
282
Ponteiros e Vetores Dinâmicos
Painel 10.2
Manipulações básicas de ponteiros ( parte 2 de 2)
16 17 18 19
p1 = new int; *p1 = 88; cout << "*p1 == " << *p1 << endl; cout << "*p2 == " << *p2 << endl;
20 21 22 }
cout << "Espero que você entenda o objetivo deste exemplo!\n"; return 0;
DIÁLOGO PROGRAMA-USUÁRIO
*p1 *p2 *p1 *p2 *p1 *p2
== == == == == ==
42 42 53 53 88 53
Espero que você entenda o objetivo deste exemplo!
Painel 10.3
Explicação do Painel 10.2
Ponteiros
283
Quando o operador new é utilizado para criar uma variável dinâmica de um tipo classe, um construtor para a classe é invocado. Caso não se especifique que construtor usar, o construtor-padrão é invocado. Por exemplo, o código seguinte invoca o construtor-padrão: UmaClasse *classePtr; classePtr = new UmaClasse; //Chama o construtor-padrão.
Se você incluir argumentos de construtor, pode-se invocar um construtor diferente, como ilustrado a seguir: classePtr =
new UmaClasse(32.0,
17); //Chama UmaClasse(double, int).
Uma notação similar permite que se inicializem variáveis dinâmicas de tipos não-classe, como ilustrado a seguir: double *dPtr;
dPtr =
new double(98.6);
// Inicializa *dPtr como 98.6.
Um tipo ponteiro é um tipo completo e pode ser utilizado da mesma forma que os outros tipos. Em particular, pode-se ter um parâmetro de função de um tipo ponteiro e pode-se ter uma função que retorne um tipo ponteiro. Por exemplo, a função seguinte possui um parâmetro que é um ponteiro para uma variável int e retorna um ponteiro (possivelmente diferente) para uma variável int: int*
encontreOutroPonteiro(int* p);
1. O que é um ponteiro em C++? 2. Indique pelo menos três usos do operador *. Nomeie e descreva cada uso. 3. Qual é a saída produzida pelo seguinte código? int *p1, *p2; p1 = new int; p2 = new int; *p1 = 10; *p2 = 20; cout << *p1 << " " << *p2 << endl; p1 = p2; cout << *p1 << " " << *p2 << endl; *p1 = 30; cout << *p1 << " " << *p2 << endl; Como a saída mudaria se você substituísse *p1 = 30; pelo seguinte? *p2 = 30; 4. Qual é a saída produzida pelo seguinte código? int *p1, *p2; p1 = new int; p2 = new int; *p1 = 10; *p2 = 20; cout << *p1 << " " << *p2 << endl; *p1 = *p2; //Esta linha é diferente do Exercício 4 cout << *p1 << " " << *p2 << endl; *p1 = 30; cout << *p1 << " " << *p2 << endl;
GERENCIAMENTO BÁSICO DE MEMÓRIA Uma região especial da memória, chamada freestore ou pilha , é reservada para variáveis dinamicamente alocadas. Qualquer nova variável dinâmica criada por um programa consome um pouco da memória da pilha. Se seu ■
284
Ponteiros e Vetores Dinâmicos
programa cria variáveis dinâmicas demais, isso consumirá toda a memória da pilha. Se isso acontecer, qualquer chamada adicional a new falhará. O que acontece quando se usa new depois de se ter esgotado toda a memória da pilha (toda a memória reservada para variáveis dinamicamente alocadas) dependerá de quão moderno é seu compilador. Com os compiladores de C++ antigos, se não houvesse memória suficiente disponível para criar uma nova variável, new forneceria um valor especial chamado NULL. Se seu compilador seguir os padrões mais novos de C++ e não houver memória suficiente disponível para criar a nova variável, o operador new encerra o programa. O Capítulo 18 discute formas de configurar seu programa para que possa adotar outro procedimento que não o de abortar quando new esgotar a pilha.2 Se seu compilador for mais antigo, você pode verificar se uma chamada a new foi bem-sucedida testando se NULL foi retornado pela chamada a new. Por exemplo, o código seguinte testa se a tentativa de criar uma nova variável dinâmica foi bem-sucedida. O programa terminará com uma mensagem de erro se a chamada a new falhar em criar a variável dinâmica desejada: int *p;
p = new int ; if (p == NULL) { cout << "Erro: Memória insuficiente.\n"; exit(1); } //Se new foi bem-sucedido, o programa continua a partir daqui.
(Lembre-se de que, como este código usa exit, requer uma instrução de include para a biblioteca com arqui vo de cabeçalho ou, em algumas implementações, .) A constante NULL é, na realidade, o número 0, mas preferimos pensar nela como NULL para deixar claro que estamos nos referindo a esse valor de finalidade especial que se pode atribuir a variáveis ponteiros. Falaremos de outros usos de NULL mais adiante neste livro. A definição do identificador NULL está em diversas bibliotecas-padrão, como e , então você deve utilizar uma instrução de include tanto para , (ou outra biblioteca adequada) quando utilizar NULL. Com dissemos, NULL é, na realidade, apenas o número 0. A definição de NULL é trabalhada pelo pré-processador do C++, que substitui NULL por 0. Assim, o compilador nunca vê "NULL" e não há namespace envolvido nem necessidade de instrução de using para NULL.3 Embora prefiramos utilizar NULL em vez de 0 em nosso código, observamos que alguns especialistas sustentam opinião contrária e advogam o uso de 0 em vez de NULL. (Não confunda o ponteiro NULL com o caractere nulo ’ \0’ utilizado para terminar strings C. São diferentes. Um é o inteiro 0, enquanto o outro é o caractere ’ \0’.) Os compiladores mais recentes não requerem a verificação explícita mencionada acima para descobrir se a nova variável dinâmica foi criada. Nos compiladores mais recentes, seu programa terminará automaticamente com uma mensagem de erro se uma chamada a new falhar em criar a variável dinâmica desejada. Entretanto, com qualquer compilador, a verificação acima não causará danos e tornará seu programa mais portátil. NULL
NULL é um valor ponteiro constante especial utilizado para dar um valor a uma variável ponteiro que, de outra forma, não possuiria valor. NULL pode ser atribuído a uma variável ponteiro de qualquer tipo. O identificador NULL está definido em diversas bibliotecas, inclusive . (A constante NULL é, na realidade, o inteiro 0.)
O tamanho da pilha varia de uma implementação de C++ para outra. Geralmente é grande, e um programa mais simples dificilmente utiliza toda a memória na pilha. Entretanto, mesmo nos programas mais simples, é uma boa prática reciclar toda memória da pilha que não é mais necessária. Se o seu programa não precisar mais de uma variável dinâmica, a memória utilizada por essa variável dinâmica pode ser devolvida ao gerenciador da pilha , 2. 3.
Tecnicamente, o operador new provoca uma exceção que, se não for detida, termina o programa. É possível deter a exceção e lidar com ela. No Capítulo 18, discutimos formas de se lidar com exceções. Os detalhes são os seguintes: a definição de NULL utiliza #define, uma forma de definição herdada da linguagem C e com a qual o pré-processador lida.
Ponteiros
285
que recicla a memória para criar outras variáveis dinâmicas. O operador delete elimina uma variável dinâmica e devol ve a memória ocupada pela variável dinâmica ao gerenciador da pilha para que possa ser reutilizada. Suponha que p seja uma variável ponteiro que esteja apontando para uma variável dinâmica. O comando seguinte destruirá a variável dinâmica apontada por p e devolverá a memória utilizada pela variável dinâmica para o gerenciador da pilha reutilizá-la: delete p;
OPERADOR delete
O operador delete elimina uma variável dinâmica e devolve a memória ocupada pela variável dinâmica à pilha. A memória pode, então, ser reutilizada para criar novas variáveis dinâmicas. Por exemplo, o código seguinte elimina a variável dinâmica apontada pelo ponteiro variable p: delete p;
Depois de uma chamada a delete, o valor da variável ponteiro, como p acima, é indefinido. (Uma versão levemente diferente de delete, de que falaremos mais tarde neste capítulo, é utilizada quando a variável dinamicamente alocada é um vetor.)
PONTEIROS OSCILANTES
Quando se aplica delete a uma variável ponteiro, a variável dinâmica para a qual ele aponta é destruída. A essa altura, o valor da variável ponteiro é indefinido, o que significa que não se sabe para onde ela está apontando. Além disso, se alguma outra variável ponteiro estava apontando para a variável dinâmica destruída, essa outra variável ponteiro também fica indefinida. Essas variáveis ponteiros indefinidas são chamadas de ponteiros oscilantes. Se p é um ponteiro oscilante e seu programa aplica o operador de desreferenciação * a p (para produzir a expressão *p), o resultado é imprevisível e normalmente desastroso. Antes de aplicar o operador de desreferenciação * a uma variável ponteiro, você deve ter certeza de que a variável ponteiro aponta para alguma variável. O C++ não possui nenhum teste interno para verificar se uma variável ponteiro é um ponteiro oscilante. Uma forma de se evitar ponteiros oscilantes é fixar qualquer variável ponteiro oscilante como igual a NULL. Então, o programa pode testar a variável ponteiro para verificar se é igual a NULL antes de aplicar o operador de desreferenciação * à variável ponteiro. Quando se utiliza essa técnica, após uma chamada a delete acrescenta-se um código para fixar todos os ponteiros oscilantes como iguais a NULL. Não se esqueça de que outras variáveis ponteiros, além daquela utilizada na chamada a delete, podem se tornar ponteiros oscilantes, então não deixe de fixar todos os ponteiros oscilantes como NULL. Cabe ao programador manter ponteiros oscilantes sob controle e fixá-los como NULL, ou garantir que não sejam desreferenciados.
VARIÁVEIS DINÂMICAS E VARIÁVEIS AUTOMÁTICAS Variáveis criadas com o operador new são chamadas de variáveis dinâmicas (ou variáveis dinamicamente aloca- das ) porque são criadas e destruídas enquanto o programa é executado. Variáveis locais — ou seja, variáveis declaradas dentro de uma definição de função — também apresentam certas características dinâmicas, mas não são chamadas de variáveis dinâmicas. Se uma variável é local a uma função, então a variável é criada pelo sistema C++ quando a função é chamada, e destruída quando a chamada de função é completada. Como a parte main de um programa é, na realidade, apenas uma função chamada main, isso também é verdade para as variáveis declaradas na parte main de seu programa. (Como a chamada a main não termina até que o programa se encerre, as variáveis declaradas em main não são destruídas até o final do programa, mas o mecanismo para lidar com variáveis locais para main é o mesmo que para outras funções.) Essas variáveis locais, às vezes, são chamadas de variáveis automáticas, porque suas propriedades dinâmicas são controladas automaticamente. Elas são criadas, automaticamente, quando a função em que foram declaradas é chamada, e destruídas, automaticamente, também quando termina a chamada de função. As variáveis declaradas fora de qualquer definição de função ou de classe, inclusive fora de main, são chamadas variáveis globais. Essas variáveis globais, às vezes, são chamadas de variáveis alocadas estaticamente, porque são realmente estáticas, em contraste com as variáveis dinâmicas e automáticas. Falamos brevemente sobre variáveis globais no Capítulo 3. Como se pode ver, não precisamos de variáveis globais e não as utilizamos. 4 ■
4.
As variáveis declaradas dentro de uma classe por meio do modificador static são estáticas em um sentido diferente do contraste dinâmico/estático que discutimos nesta seção.
286
Ponteiros e Vetores Dinâmicos
DEFINA TIPOS PONTEIRO
Pode-se definir um nome de tipo ponteiro de modo que as variáveis ponteiros possam ser declaradas como outras variáveis sem a necessidade de colocar um asterisco diante de cada variável ponteiro. Por exemplo, o código seguinte define um tipo IntPtr, que é o tipo para variáveis ponteiros que contém ponteiros para variáveis int: typedef int * IntPtr;
Assim, as duas declarações de variável ponteiro seguintes são equivalentes: IntPtr p;
e int *p;
Pode-se utilizar typedef para definir um outro nome (alias) para qualquer nome ou definição de tipo. Por exemplo, o código seguinte define o nome de tipo Quilometros para significar o mesmo que o nome de tipo double: typedef double Quilometros;
Uma vez que se tenha dado essa definição de tipo, pode-se definir uma variável de tipo double da seguinte forma: Quilometros distancia;
Renomear tipos existentes dessa forma, às vezes, é útil. Entretanto, nosso uso principal para typedef será definir tipos para variáveis ponteiros. Não se esqueça de que um typedef não produz um novo tipo; é apenas um outro nome para a definição do tipo. Por exemplo, dada a definição prévia de Quilometros, uma variável de tipo Quilometros pode ser substituída por um parâmetro de tipo double. Quilometros e double são dois nomes para o mesmo tipo. Existem duas vantagens em utilizar nomes de tipo de ponteiro definidos, como o IntPtr definido anteriormente. Primeiro, evita-se o erro de omitir um asterisco. Lembre que, se você quer que p1 e p2 sejam ponteiros, a declaração seguinte é um erro: int *p1, p2;
Como o * foi omitido em p2, a variável p2 é apenas uma variável int comum, não uma variável ponteiro. Se você se confundir e colocar o * no int, o problema é o mesmo, mas é mais difícil de notar. O C++ permite que você coloque o * no nome do tipo, como int, de modo que a seguinte declaração é legal: int* p1, p2;
Embora seja legal, causa confusões. Faz parecer que tanto p1 quanto p2 são variáveis ponteiros, mas na realidade apenas p1 é uma variável ponteiro; p2 é uma variável int comum. No que se refere ao compilador de C++, o * ligado ao identificador int pode estar ligado ao identificador p1. Uma forma correta de se declarar tanto p1 quanto p2 como variáveis ponteiros é int *p1, *p2;
Uma forma mais fácil e menos sujeita a erros de declarar tanto p1 quanto p2 como variáveis ponteiros é utilizar o nome de tipo definido IntPtr, assim: IntPtr p1, p2;
A segunda vantagem de se utilizar um tipo de ponteiro definido, como IntPtr, é vista quando se define uma função com um parâmetro chamado por referência para uma variável ponteiro. Sem o nome do tipo de ponteiro definido, você precisaria incluir tanto um * quanto um & na declaração para a função, e os detalhes podem se tornar confusos. Se você utilizar um nome de tipo para o tipo ponteiro, e um parâmetro chamado por referência para um tipo ponteiro não criará dificuldades. Define-se um parâmetro chamado por referência para um tipo ponteiro definido exatamente como se define qualquer outro parâmetro chamado por referência. Aqui está um exemplo: void funcaoExemplo(IntPtr& ponteiroVariavel);
5. Que infeliz engano pode ocorrer com a declaração seguinte? int* intPtr1, intPtr2;
6. Suponha que uma variável dinâmica fosse criada assim: char *p;
p = new char ;
Presumindo que o valor da variável ponteiro p não houvesse mudado (portanto, ainda aponta para a mesma variável dinâmica), como se pode destruir essa variável dinâmica e devolver a memória que ela utilizava para o gerenciador de memória de modo que a memória possa ser reutilizada para criar novas variáveis dinâmicas?
Ponteiros
287
7. Escreva uma definição para um tipo chamado NumeroPtr que será o tipo para variáveis ponteiros que guardam ponteiros para variáveis dinâmicas de tipo double. Além disso, escreva uma declaração para uma variável ponteiro chamada meuPonteiro, que é do tipo NumeroPtr. 8. Descreva a ação do operador new. O que o operador new retorna? Quais são as indicações de erros?
DEFINIÇÕES DE TIPO Pode-se atribuir um nome a uma definição de tipo e, então, utilizar o nome do tipo para declarar variáveis. Isso é feito com a palavra-chave typedef. Essas definições de tipo normalmente são colocadas fora do corpo da parte main do programa e fora do corpo de outras funções, em geral perto do início de um arquivo. Dessa forma o typedef é global e disponível para todo o programa. Utilizaremos definições de tipo para definir nomes para tipos ponteiro, como mostra o exemplo a seguir.
SINTAXE typedef Definicao_Do_Tipo_Conhecido Novo_Nome_Do_Tipo;
EXEMPLO typedef int * IntPtr;
O nome do tipo IntPtr pode ser usado para declarar ponteiros para variáveis dinâmicas de tipo int, como no seguinte exemplo: IntPtr ponteiro1, ponteiro2;
PONTEIROS COMO PARÂMETROS CHAMADOS POR VALOR Quando um parâmetro chamado por valor é de um tipo ponteiro, seu comportamento pode, ocasionalmente, ser imprevisível e problemático. Considere a chamada de função mostrada no Painel 10.4. O parâmetro temp na função enganoso é um parâmetro chamado por valor e, assim, é uma variável local. Quando a função é chamada, o valor de temp é fixado como o valor do argumento p, e o corpo da função é executado. Como temp é uma variável local, nenhuma alteração a temp deve ir fora da função enganoso. Em particular, o valor da variável ponteiro p não deve ser alterado. No entanto, o diálogo programa-usuário dá a entender que o valor da variável ponteiro p mudou. Antes da chamada à função enganoso, o valor de *p era 77, e, depois da chamada a enganoso, o valor de *p é 99. O que aconteceu? A situação está esquematizada no Painel 10.5. Embora o diálogo programa-usuário dê a entender que p foi alterado, o valor de p não foi alterado pela chamada à função enganoso. O ponteiro p apresenta duas associações: o valor do ponteiro p e o valor armazenado no local para onde p aponta. Mas o valor de p é o ponteiro (isto é, um endereço de memória). Depois da chamada a enganoso, a variável p contém o mesmo valor de ponteiro (ou seja, o mesmo endereço de memória). A chamada a enganoso alterou o valor da variá vel apontada por p, mas não alterou o valor do próprio p. Se o tipo do parâmetro é uma classe ou tipo-estrutura que possui variáveis-membros de um tipo-ponteiro, a mesma espécie de mudança surpreendente pode ocorrer com argumentos chamados por valor do tipoclasse. Entretanto, para tipos-classe podem-se evitar (e controlar) essas mudanças surpreendentes definindo-se um construtor de cópia, como descreveremos mais adiante neste capítulo.
Painel 10.4
Parâmetro ponteiro chamado por valor ( parte 1 de 2)
1 2 3 4 5 6
//Programa para demonstrar o modo como os parâmetros chamados por valor //se comportam com argumentos de ponteiros. #include using std::cout; using std::cin; using std::endl;
7
typedef int * IntPointer;
8
void sneaky(IntPointer temp);
9 int main( ) 10 { 11 IntPointer p; 12
p = new int ;
288
Ponteiros e Vetores Dinâmicos
Painel 10.4
Parâmetro ponteiro chamado por valor ( parte 2 de 2)
13 14 15
*p = 77; cout << "Antes da chamada à função *p == " << *p << endl;
16
sneaky(p);
17 18
cout << "Depois da chamada à função *p == " << *p << endl;
19 20 21 22 23 24 25 26
return 0; } void sneaky(IntPointer temp) { *temp = 99; cout << "Dentro da chamada de função *temp == " << *temp << endl; }
DIÁLOGO PROGRAMA-USUÁRIO Antes da chamada à função *p == 77 Dentro da chamada de função *temp == 99 Depois da chamada à função *p == 99
Painel 10.5
Chamada à função enganoso(p);
■ USOS PARA PONTEIROS
O Capítulo 17 trata de formas de se utilizar ponteiros para criar diversas estruturas de dados úteis. Este capítulo discute apenas um dos usos dos ponteiros, o de referenciar vetores e, em particular, criar e referenciar uma espécie de vetor conhecida como vetor dinamicamente alocado . Vetores dinamicamente alocados são o assunto da Seção 10.2.
10.2
Vetores Dinâmicos
Nesta seção você verá que variáveis vetores são, na verdade, variáveis ponteiros. Você também descobrirá como escrever programas com vetores dinamicamente alocados. Um vetor dinamicamente alocado (também chamado
Vetores Dinâmicos
289
apenas de vetor dinâmico ) é um vetor cujo tamanho não é especificado quando se escreve o programa, mas é determinado enquanto o programa é executado. ■
VARIÁVEIS VETORES E VARIÁVEIS PONTEIROS
O Capítulo 5 descreveu como os vetores são guardados na memória. Àquela altura, tratamos de vetores em termos de endereços de memória. Mas um endereço de memória é um ponteiro. Assim, em C++ uma variável vetor é, na realidade, uma espécie de variável ponteiro que aponta para a primeira variável indexada do vetor. Dadas as duas seguintes declarações de variável, p e a são ambas variáveis ponteiros: int a[10]; typedef int * IntPtr;
IntPtr p;
O fato de a e p serem ambas variáveis ponteiros é ilustrado no Painel 10.6. Como a é um ponteiro que aponta para uma variável de tipo int (a saber, a variável a[0]), o valor de a pode ser atribuído à variável ponteiro p da seguinte forma: p = a;
Depois dessa atribuição, p aponta para a mesma posição de memória apontada por a. Assim, p[0], p[1], . . . p[9] se referem às variáveis indexadas a[0], a[1], . . . a[9]. A notação de colchetes que você vem utilizando para vetores se aplica a variáveis ponteiros desde que a variável ponteiro aponte para um vetor na memória. Depois da atribuição acima, pode-se tratar o identificador p como se fosse um identificador de vetor. Pode-se, também, tratar o identificador a como se fosse uma variável ponteiro, mas há uma restrição importante: não se pode alterar o valor do ponteiro em uma variável vetor . Se a variável ponteiro p2 tem um valor, você pode ser levado a pensar que o seguinte código é legal, mas não é: a = p2;//ILEGAL. Não se pode atribuir um endereço diferente a a.
O motivo por que essa atribuição não funciona é que uma variável vetor não é de tipo int*, mas seu tipo é uma versão const de int*. Uma variável vetor, como a, é uma variável ponteiro com o modificador const, o que significa que seu valor não pode ser alterado. (Uma variável vetor de fato é mais do que uma variável ponteiro comum, já que carrega informações adicionais de tamanho sobre o vetor, mas uma variável vetor inclui um ponteiro para o vetor e pode ser atribuída a uma variável ponteiro. Assim, uma variável vetor é uma espécie de variável ponteiro e pode ser tratada como uma variável ponteiro cujo valor não possa ser alterado.) Painel 10.6 Vetores e variáveis ponteiros ( parte 1 de 2) 1 2 3 4
//Programa para demonstrar que uma variável vetor é um tipo de variável ponteiro. #include using std::cout; using std::endl;
5
typedef int * IntPtr;
6 int main( ) 7 { 8 IntPtr p; 9 int a[10]; 10 int index; 11 12
for (index = 0; index < 10; index++)
13
p = a;
14 15
for (index = 0; index < 10; index++)
a[index] = index;
cout << p[index] << " ";
290
Ponteiros e Vetores Dinâmicos
Painel 10.6 Vetores e variáveis ponteiros ( parte 2 de 2) 16
cout << endl;
17 18
for (index = 0; index < 10; index++) p[index] = p[index] + 1;
19 20 21
for (index = 0; index < 10; index++) cout << a[index] << " "; cout << endl;
22 23 }
return 0;
Observe que as alterações no vetor p são também alterações no a.
DIÁLOGO PROGRAMA-USUÁRIO 0 1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 10
CRIANDO E UTILIZANDO VETORES DINÂMICOS Um problema com as espécies de vetores discutidos no Capítulo 5 é a necessidade de especificar o tamanho do vetor quando se escreve o programa — mas talvez você não saiba de que tamanho de vetor precisará até o programa ser executado. Por exemplo, um vetor pode guardar uma lista de números de identificação de estudantes, mas o tamanho da classe pode ser diferente cada vez que o programa é executado. Com as espécies de vetores que utilizamos até agora, é preciso estimar o maior tamanho possível necessário para o vetor e esperar que esse tamanho seja grande o bastante. Há dois problemas com isso. Primeiro, sua estimativa pode ser muito baixa, e o programa não funcionará em todas as situações. Segundo, como o vetor pode ter muitas posições não-utilizadas, isso pode acarretar um desperdício de memória. Vetores dinamicamente alocados evitam esses problemas. Se seu programa utiliza um vetor dinamicamente alocado para números de identificação de estudantes, o número de estudantes pode ser fornecido como entrada para o programa, e o vetor dinamicamente alocado pode ser criado com um tamanho exatamente igual ao número de estudantes. Vetores dinamicamente alocados são criados utilizando o operador new. A criação e o uso de vetores dinamicamente alocados é surpreendentemente simples. Como variáveis vetores são variáveis ponteiros, pode-se utilizar o operador new para criar variáveis dinamicamente alocadas que são vetores e tratar esses vetores dinamicamente alocados como se fossem vetores comuns. Por exemplo, o código seguinte cria uma variável vetor dinamicamente alocada com dez elementos de vetor de tipo double: ■
typedef double * DoublePtr; DoublePtr d; d = new double [10];
Para obter um vetor dinamicamente alocado de elementos de qualquer outro tipo, é só substituir double pelo tipo desejado. Em particular, pode-se substituir o tipo double por um tipo struct ou classe. Para obter uma variável vetor dinamicamente alocada de qualquer outro tamanho, é só substituir o 10 pelo tamanho desejado. Existem também diversos aspectos menos óbvios a observar em relação a esse exemplo. Primeiro, o tipo ponteiro que você utiliza para um ponteiro em um vetor dinamicamente alocado é o mesmo que usaria em um único elemento do vetor. Por exemplo, o tipo ponteiro para um vetor de elementos de tipo double é o mesmo que de veria usar em uma simples variável de tipo double. O ponteiro para o vetor é, na realidade, um ponteiro para a primeira variável indexada do vetor. No exemplo apresentado, um vetor inteiro com dez variáveis indexadas é criado, e se mantém o ponteiro p apontando para a primeira dessas dez variáveis indexadas. Segundo, observe que, quando você chama new, o tamanho do vetor dinamicamente alocado é dado entre colchetes após o tipo, que neste exemplo é double. Isso diz ao computador quanto espaço reservar para o vetor dinâmico.
Vetores Dinâmicos
291
Se você omitisse os colchetes e o 10 nesse exemplo, o computador alocaria espaço suficiente para apenas uma variável de tipo double, e não para um vetor de dez variáveis indexadas de tipo double. O Painel 10.7 contém um programa que ilustra o uso de um vetor dinamicamente alocado. O programa faz uma busca em uma lista de números armazenados em um vetor dinamicamente alocado. O tamanho do vetor é determinado quando o programa é executado. O programa pergunta ao usuário quantos números haverá e, então, o operador new cria um vetor dinamicamente alocado daquele tamanho. O tamanho do vetor dinâmico é dado pela variável tamanhoDoVetor. O tamanho de um vetor dinâmico não precisa ser dado por uma constante. Pode, como no Painel 10.7, ser dado por uma variável cujo valor é determinado quando o programa é executado. Painel 10.7
Vetor dinamicamente alocado ( parte 1 de 2)
1 2 3 4
//Efetua busca em uma lista de números digitados ao teclado. #include using std::cin; using std::cout;
5
typedef int * IntPtr;
6 7 8 9
void fillArray(int a[], int size);
Parâmetros de
//Pré-condição: size é o tamanho declarado do vetor a. //Pós-condição: a[0] até a[size-1] foram //preenchidos com valores lidos a partir do teclado.
vetor ordinário
10 11 12 13 14
int search(int a[], int size, int target);
//Pré-condição: size é o tamanho declarado do vetor a. //O vetor de elementos a[0] até a[size-1] possui valores. //Se target estiver no vetor, retorna o primeiro índice de target. //Se target não estiver no vetor, retorna -1.
15 int main( ) 16 { 17 cout << "Este programa efetua uma busca em uma lista de números.\n"; 18 19 20 21 22
int arraySize;
23
fillArray(a, arraySize);
24 25 26 27 28 29 30 31 32 33 34 35 36 }
cout << "Quantos números haverá na lista? "; cin >> arraySize; IntPtr a; a = new int [arraySize];
int target;
O vetor dinâmico
a é
utilizado
como um vetor ordinário.
cout << "Digite um valor a ser procurado: "; cin >> target; int location = search(a, arraySize, target); if (location == -1) cout << target << " não está no vetor.\n"; else
cout << target << " é o elemento " << location << " no vetor.\n"; delete [] a; return 0;
37 //Utiliza a biblioteca : 38 void fillArray(int a[], int size) 39 { 40 cout << "Digite " << size << " inteiros.\n";
292
Ponteiros e Vetores Dinâmicos
Painel 10.7
41 42 43 }
Vetor dinamicamente alocado ( parte 2 de 2)
for (int index
= 0; index < size; index++) cin >> a[index];
44 int search(int a[], int size, int target) 45 { 46 int index = 0; 47 while ((a[index] != target) && (index < size)) 48 index++; 49 if (index == size)//se target não estiver em a. 50 index = -1; 51 return index; 52 }
DIÁLOGO PROGRAMA-USUÁRIO Este programa efetua uma busca em uma lista de números. Quantos números haverá na lista? 5 Digite 5 inteiros. 1 2 3 4 5
Digite um valor a ser procurado: 3 3 é o elemento 2 no vetor.
Observe o comando delete, que destrói o vetor dinamicamente alocado apontado por a no Painel 10.7. Como o programa está prestes a terminar, na realidade não precisamos desse comando delete; se o programa continuasse e fizesse outras coisas, todavia, esse comando delete seria desejável, para que a memória utilizada por esse vetor dinamicamente alocado fosse devolvida ao gerenciador da pilha. O comando delete para um vetor dinamicamente alocado é similar ao comando delete que vimos anteriormente, a não ser pelo fato de que, com um vetor dinamicamente alocado, se deve incluir um par de colchetes: delete []
a;
Os colchetes dizem ao C++ que uma variável vetor dinamicamente alocado é eliminada, então o sistema verifica o tamanho do vetor e remove aquela quantidade de variáveis indexadas. Se você omitir os colchetes, não estará eliminando o vetor inteiro. Por exemplo: delete a;
não é legal, mas o erro não é detectado pela maioria dos compiladores. O padrão C++ diz que o que acontece quando se faz isso é "indefinido". Isso significa que o autor do compilador pode fazer com isso o que achar con veniente (para o criador do compilador, não para você). Utilize sempre a sintaxe delete []
vetorPtr;
quando for apagar a memória que tiver sido alocada por algo como vetorPtr =
MeuTipo[37]; new
Observe também a posição dos colchetes no comando delete delete []
vetorPtr; //Correto //ILEGAL!
delete vetorPtr[];
Cria-se um vetor dinamicamente alocado com uma chamada a new utilizando um ponteiro, como o ponteiro a no Painel 10.7. Depois da chamada a new, não se deve atribuir nenhum outro valor ponteiro a essa variável ponteiro, porque isso pode confundir o sistema quando a memória para o vetor dinâmico for devolvida ao gerenciador da pilha com uma chamada a delete.
Vetores Dinâmicos
293
Vetores dinamicamente alocados são criados com new e uma variável ponteiro. Quando seu programa é encerrado por meio de um vetor dinamicamente alocado, você deve devolver a memória do vetor ao gerenciador da pilha com uma chamada a delete. Fora isso, um vetor dinamicamente alocado pode ser utilizado como qualquer outro vetor. FUNÇÃO QUE RETORNA UM VETOR Em C++, não é permitido que um tipo vetor seja retornado por uma função. Por exemplo, a linha seguinte é ilegal: int [] umaFuncao( );//ILEGAL Se você quiser criar uma função semelhante a essa, deve retornar um ponteiro para o tipo-base do vetor e fazer com que o ponteiro aponte para o vetor. Assim, a declaração de função seria: int* umaFuncao( );//Legal Um exemplo de função que retorna um ponteiro para um vetor é dado no Painel 10.8.
9. Escreva uma definição de tipo para variáveis ponteiros que serão usadas para apontar para vetores dinamicamente alocados. Os elementos dos vetores devem ser do tipo char. Chame o tipo de VetorChar. 10. Suponha que seu programa contenha código para criar um vetor dinamicamente alocado da seguinte forma: int *entrada; entrada = new int[10]; de forma que a variável ponteiro entrada aponte para esse vetor dinamicamente alocado. Escreva código para preencher esse vetor com dez números digitados no teclado. 11. Suponha que seu programa contenha código para criar um vetor dinamicamente alocado como no Exercício de Autoteste 10, e suponha que a variável ponteiro entrada não tenha seu valor (ponteiro) alterado. Escreva código para destruir esse vetor dinamicamente alocado e devolver a memória utilizada por ele ao gerenciador da pilha. 12. Qual é a saída do seguinte fragmento de código? int a[10]; int tamanhoDoVetor = 10; int *p = a; int i; for (i = 0; i < tamanhoDoVetor; i++) a[i] = i; for (i = 0; i < tamanhoDoVetor; i++) cout << p[i] << " "; cout << endl;
Painel 10.8
Retornando um ponteiro para um vetor ( parte 1 de 2)
1 #include 2 using std::cout; 3 using std::endl; 4 5 6 7 8
int* doubler(int a[], int size);
//Pré-condição: size é o tamanho declarado do vetor a. //Todos os valores indexados de a possuem valores. //Retorna: um ponteiro para um vetor de mesmo tamanho que a em que //cada variável indexada reproduz o elemento correspondente em a.
9 int main( ) 10 { 11 int a[] = {1, 2, 3, 4, 5}; 12 int* b;
294
Ponteiros e Vetores Dinâmicos
Painel 10.8
Retornando um ponteiro para um vetor ( parte 2 de 2)
13
b = doubler(a, 5);
14 15 16 17 18 19 20 21 22
int i; cout << "Vetor a:\n"; for (i = 0; i < 5; i++) cout << a[i] << " "; cout << endl; cout << "Vetor b:\n"; for (i = 0; i < 5; i++) cout << b[i] << " "; cout << endl;
23 24 25 }
delete[] b; return 0;
26 int* doubler(int a[], int size) 27 { 28 int* temp = new int[size]; 29 30
for (int i =0; i < size; i++) temp[i] = 2*a[i];
31 32 }
return temp;
Esta chamada a delete não é, de fato, necessária, já que o programa está se encerrando, mas em outro contexto seria importante incluir esse delete.
DIÁLOGO PROGRAMA-USUÁRIO Vetor 1 2 3 Vetor 2 4 6
■
a: 4 5 b: 8 10
ARITMÉTICA DE PONTEIROS
Pode-se efetuar um tipo de aritmética de ponteiros, mas é uma aritmética de endereços, não de números. Por exemplo, suponha que seu programa contenha o seguinte código: typedef double * DoublePtr; DoublePtr d; d = new double [10];
Depois desses comandos, d contém o endereço da variável indexada d[0]. A expressão d + 1 calcula o endereço de d[1], d + 2 é o endereço de d[2], e assim por diante. Observe que, embora o valor de d seja um endereço e um endereço seja um número, d + 1 não acrescenta simplesmente 1 ao número em d. Se uma variável do tipo double exigir oito bytes (oito posições de memória) e d contiver o endereço 2000, então d + 1 apresenta como resultado o endereço de memória 2008. É claro que o tipo double pode ser substituído por qualquer outro tipo, e assim a adição do ponteiro se move em unidades de variáveis daquele tipo. Esta aritmética de ponteiros representa uma forma alternativa de se manipular vetores. Por exemplo, se tamanhoDoVetor for o tamanho do vetor dinamicamente alocado apontado por d, o seguinte código apresentará como saída o conteúdo do vetor dinâmico: for (int i = 0; i < tamanhoDoVetor; i++) cout << *(d + i)<< " ";
Vetores Dinâmicos
295
O código apresentado anteriormente é equivalente a: for (int i = 0; i < tamanhoDoVetor; i++) cout << d[i] << " ";
Não se pode executar multiplicação ou divisão de ponteiros. Só se pode acrescentar um inteiro a um ponteiro, subtrair um inteiro de um ponteiro ou subtrair dois ponteiros de mesmo tipo. Quando se subtraem dois ponteiros, o resultado é o número de variáveis indexadas entre os dois endereços. Lembre-se, para subtrair dois valores de ponteiro, esses valores devem apontar para o mesmo vetor ! Não faz sentido subtrair um ponteiro que aponta para um vetor de outro que aponta para um vetor diferente. Também se podem utilizar os operadores de incremento e decremento, ++ e --, para executar aritmética de ponteiros. Por exemplo, d++ fará com que o valor de d avance de modo que contenha o endereço da próxima variável indexada, e d-- fará com que d passe a conter o endereço da variável indexada anterior. ■
VETORES DINÂMICOS MULTIDIMENSIONAIS
É possível termos vetores dinâmicos multidimensionais. Não se pode esquecer, contudo, de que os vetores multidimensionais são vetores de vetores de vetores de vetores de vetores, e assim por diante. Por exemplo, para criar um vetor dinâmico bidimensional, você deve se lembrar de que este é um vetor de vetores. Para criar um vetor de inteiros bidimensional, primeiro se cria um vetor dinâmico unidimensional de ponteiros de tipo int*, que é o tipo para vetores unidimensionais de ints. Assim, cria-se um vetor dinâmico de ints para cada elemento do vetor. Uma definição de tipo pode ajudar a manter a ordem. Eis o tipo de variável para um vetor dinâmico unidimensional ordinário de ints: typedef int* IntVetorPtr;
Para obter um vetor de três por quatro de
ints,
exige-se um vetor cujo tipo-base seja
IntVetorPtr.
Por exemplo:
IntVetorPtr *m = new IntVetorPtr[3];
Este é um vetor de três ponteiros, cada um dos quais pode nomear um vetor dinâmico de forma:
ints,
da seguinte
for (int i = 0; i < 3; i++) m[i] = new int[4];
COMO UTILIZAR UM VETOR DINÂMICO ■
Defina um tipo ponteiro: defina um tipo para ponteiros para variáveis do mesmo tipo que os elementos do vetor. Por exemplo, se o vetor dinâmico é um vetor de doubles, utilize: typedef double * DoubleVetorPtr;
■
Declare uma variável ponteiro: declare uma variável ponteiro desse tipo definido. A variável ponteiro apontará para o vetor dinamicamente alocado na memória e servirá como nome do vetor dinâmico. DoubleVetorPtr a;
■
(Ou, então, sem um tipo ponteiro definido, use double *a; .) Chame new: crie um vetor dinâmico utilizando o operador new: a = new double [tamanhoDoVetor];
■
■
O tamanho do vetor dinâmico é dado entre colchetes, como no exemplo anterior. O tamanho pode ser dado por meio de uma variável int ou outra expressão int. No exemplo apresentado, tamanhoDoVetor pode ser uma variável de tipo int cujo valor é determinado enquanto o programa apresentado é executado. Utilize como se fosse um vetor comum: a variável ponteiro, como a, é utilizada como um vetor comum. Por exemplo, as variáveis indexadas são escritas na forma usual: a[0], a[1], e assim por diante. A variável ponteiro não deve ter nenhum outro valor de ponteiro atribuído a ela; deve ser usada como uma variável vetor. Chame delete[] : quando seu programa terminar com a variável vetor dinamicamente alocado, utilize delete e esvazie os colchetes com a variável ponteiro para eliminar o vetor dinâmico e devolver o espaço ocupado para o gerenciador da pilha reutilizá-lo. Por exemplo: delete [] a;
O vetor resultante m é um vetor dinâmico de três por quatro. Um programa simples para ilustrar isso é fornecido no Painel 10.9.
296
Ponteiros e Vetores Dinâmicos
Repare na utilização de delete no Painel 10.9. Como o vetor dinâmico m é um vetor de vetores, cada um dos vetores criados com new no loop for nas linhas 13 e 14 deve ser devolvido ao gerenciador da pilha com uma chamada a delete []; então, o próprio vetor m deve ser devolvido ao gerenciador da pilha com outra chamada a delete []. Deve haver uma chamada a delete [] para cada chamada a new que criou um vetor. (Como o programa termina logo após as chamadas a delete [], poderíamos omitir essas chamadas sem correr riscos, mas preferimos ilustrar o uso de delete [].) Painel 10.9
Vetor dinâmico bidimensional ( parte 1 de 2)
1 #include 2 using std::cin; 3 using std::cout; 4 using std::endl;
5
typedef int * IntArrayPtr;
6 int main( ) 7 { 8 int d1, d2; 9 cout << "Informe as dimensões linha e a coluna do vetor:\n"; 10 cin >> d1 >> d2;
11 12 13 14 15
IntArrayPtr *m = new IntArrayPtr[d1]; int i, j; for (i = 0; i < d1; i++) m[i] = new int[d2];
16 17 18 19 20
cout << "Digite " << d1 << " linhas de " << d2 << " inteiros cada:\n"; for (i = 0; i < d1; i++) for (j = 0; j < d2; j++) cin >> m[i][j];
21 22 23 24 25 26 27 28 29 30 31
//m é agora um vetor d1-por-d2.
cout << "Ecoando o vetor bidimensional:\n"; for (i = 0; i < d1; i++)
{ for (j = 0; j < d2; j++)
cout << m[i][j] << " "; cout << endl; } for (i = 0; i < d1; i++) delete [] m[i]; delete[] m;
Observe que é necessário haver uma chamada a delete [ ] para cada chamada a new que criou um vetor. (Essas chamadas a delete [ ] não são realmente necessárias, já que o programa está se encerrando, mas em outro contexto seria importante incluí-las.)
32 return 0; 33 }
DIÁLOGO PROGRAMA-USUÁRIO Informe as dimensões linha e as colunas do vetor: 3 4
Digite 3 linhas de 4 inteiros cada:
Classes, Ponteiros e Vetores Dinâmicos
Painel 10.9
297
Vetor dinâmico bidimensional ( parte 2 de 2)
1 2 3 4 5 6 7 8 9 0 1 2
Ecoando o vetor bidimensional: 1 2 3 4 5 6 7 8 9 0 1 2
10.3
Classes, Ponteiros e Vetores Dinâmicos As combinações são infinitas. Jargão de publicidade
Um vetor dinamicamente alocado pode ter um tipo-base que seja uma classe. Uma classe pode ter uma variá vel-membro que seja um vetor dinamicamente alocado. Podem-se combinar classes e vetores dinamicamente alocados de inúmeras formas. Existem algumas fontes de preocupação quando se utilizam classes e vetores dinamicamente alocados, mas as técnicas básicas são aquelas que você já vem utilizando. Muitas das técnicas apresentadas nesta seção se aplicam a todas as estruturas dinamicamente alocadas, como as que discutiremos no Capítulo 17, e não apenas a classes envolvendo vetores dinamicamente alocados. ■ OPERADOR
–>
O C++ possui um operador que pode ser usado com um ponteiro para simplificar a notação para especificar os membros de um struct ou uma classe. O operador seta , ->, combina as ações de um operador de desreferenciação, *, e um operador ponto para especificar um membro de um struct ou objeto classe dinâmica que seja apontado por um dado ponteiro. Por exemplo, se tivermos a seguinte definição: struct Registro
{ int numero; char nota;
};
O código seguinte cria uma variável dinamicamente alocada de tipo Registro e fixa as variáveis-membros da variável struct dinâmica como 2001 e ’A’: Registro *p; p = new Registro; p->numero = 2001; p->nota = ’A’;
As notações p->nota
e (*p).nota
têm o mesmo significado. Entretanto, a primeira é a mais conveniente e utilizada. ■ PONTEIRO this
Quando se definem funções-membros para uma classe, muitas vezes queremos nos referir ao objeto que faz a chamada. O ponteiro this é um ponteiro predefinido que aponta para o objeto que faz a chamada. Por exemplo, considere uma classe como a seguinte:
298
Ponteiros e Vetores Dinâmicos
class Amostra
{ public:
... void mostraAlgo( ) const;
... private: int algo;
... };
As duas formas seguintes de definir a função-membro mostraAlgo são equivalentes: void Amostra::mostraAlgo( ) const
{ cout << algo; } //O estilo é ruim, mas ilustra o ponteiro this: void Amostra::mostraAlgo( ) { cout << this->algo; }
Observe que this não é o nome do objeto que faz a chamada, e sim o nome de um ponteiro que aponta para o objeto que faz a chamada. O ponteiro this não pode ter seu valor alterado; sempre aponta para o objeto que faz a chamada. Como nosso comentário anterior indicou, normalmente não há necessidade do ponteiro this. Em algumas situações, contudo, ele é útil. O ponteiro this em geral é usado, por exemplo, na sobrecarga do operador de atribuição, =, de que trataremos a seguir. Como o ponteiro this aponta para o objeto que faz a chamada, não se pode utilizar this na definição de nenhuma função-membro estática. Uma função-membro estática normalmente não possui objeto que faz chamadas para o qual o ponteiro this pudesse apontar. ■
SOBRECARREGANDO O OPERADOR DE ATRIBUIÇÃO
Neste livro, geralmente utilizamos o operador de atribuição como se fosse uma função void. Entretanto, o operador de atribuição predefinido retorna uma referência que permite algumas utilizações especializadas. Com o operador de atribuição predefinido, é possível encadear operadores de atribuição da seguinte forma: a = b = c;, que significa a = (b = c); . A primeira operação, b = c, retorna a nova versão de b. Assim, a ação de a = b = c;
é fixar a, assim como b, como igual a c. Para garantir que suas versões sobrecarregadas do operador de atribuição possam ser utilizadas dessa forma, você precisa definir o operador de atribuição para que retorne algo do mesmo tipo que seu lado esquerdo. Como logo você verá, o ponteiro this lhe permitirá fazer isso. Entretanto, embora seja necessário que o operador de atribuição retorne algo do tipo de seu lado esquerdo, não é necessário que retorne uma referência. Outro uso do operador de atribuição explica por que uma referência é fornecida. O motivo por que o operador de atribuição predefinida retorna uma referência é que se pode invocar uma função-membro com o valor retornado, como em (a = b).f( );
em que f é uma função-membro. Se você quiser que suas versões sobrecarregadas do operador de atribuição permitam a invocação de funções-membros dessa forma, deve fazer com que elas retornem uma referência. Esta não é uma razão muito atraente para se retornar uma referência, já que é uma propriedade não tão importante e raramente utilizada. Entretanto, faz parte da tradição retornar uma referência, e não é muito mais difícil retornar uma referência do que apenas retornar um valor.
Classes, Ponteiros e Vetores Dinâmicos
299
Por exemplo, considere a seguinte classe (que poderia ser utilizada para lidar com uma string especializada, com a qual não seria tão fácil lidar por meio da classe predefinida string): class ClasseString
{ public :
... void algumProcessamento( );
... ClasseString& operator=(const ClasseString& ladoDir); ... private : char *a;//Vetor dinâmico para caracteres na string int capacidade;//tamanho do vetor dinâmico a int comprimento;//Número de caracteres em a };
Como foi observado no Capítulo 8, quando se sobrecarrega o operador de atribuição, ele deve ser um membro da classe; não pode ser um amigo da classe. Esta é a razão por que a definição anterior apresenta apenas um parâmetro para o operador. Por exemplo: s1 = s2;//s1 e s2 na classe ClasseString
Na chamada anterior, s1 é o objeto que faz a chamada e s2 é o argumento para o operador-membro =. A seguinte definição do operador de atribuição sobrecarregado pode ser usada no encadeamento de atribuições como s1 = s2 = s3;
e pode ser usada para invocar funções-membros, da seguinte forma: (s1 = s2).algumProcessamento( );
A definição do operador de atribuição sobrecarregado utiliza o ponteiro this para retornar o objeto no lado esquerdo do sinal = (que é o objeto que faz a chamada): //Esta versão não funciona em todos os casos. ClasseString& ClasseString::operator=(const ClasseString& ladoDir) { capacidade = ladoDir.capacidade; comprimento = ladoDir.comprimento; delete [] a; a = new char [capacidade]; for (int i = 0; i < comprimento; i++) a[i] = ladoDir.a[i]; return *this;
}
Esta versão apresenta um problema quando utilizada em uma atribuição com o mesmo objeto de ambos os lados do operador de atribuição, como: s = s;
Quando essa atribuição é executada, o seguinte comando é executado: delete [] a;
Mas o objeto que faz a chamada é s, então isso significa delete [] s.a;
O ponteiro s.a é indefinido. O operador de atribuição corrompeu o objeto s, e essa execução do programa provavelmente foi arruinada.
300
Ponteiros e Vetores Dinâmicos
Para muitas classes, a definição óbvia para sobrecarregar o operador de atribuição não funciona corretamente quando o mesmo objeto está em ambos os lados do operador de atribuição. Você deve sempre verificar e cuidar para escrever sua definição do operador de atribuição sobrecarregado para que funcione também nesse caso. Para evitar o problema que tivemos com nossa primeira definição do operador de atribuição sobrecarregado, você pode utilizar o ponteiro this para testar esse caso especial, da seguinte forma: //Versão final, com erro corrigido: ClasseString& ClasseString::operator=(const ClasseString& ladoDir) { if (this == &ladoDir) //se o lado direito for igual ao lado esquerdo { return *this; } else
{ capacidade = ladoDir.capacidade; comprimento = ladoDir.comprimento; delete [] a; a = new char[capacidade]; for (int i = 0; i < comprimento; i++) a[i] = ladoDir.a[i]; return *this;
} }
Um exemplo completo com um operador de atribuição sobrecarregado é dado no próximo exemplo de programação. CLASSE PARA VETORES PARCIALMENTE PREENCHIDOS
A classe VetorPPD, nos Painéis 10.10 e 10.11, é uma classe para um vetor parcialmente preenchido de doubles.5 Como mostrado no programa de demonstração no Painel 10.12, pode-se ter acesso a um objeto de uma classe VetorPPD utilizando-se colchetes, como um vetor normal, mas o objeto também controla automaticamente quanto do vetor é utilizado. Assim, ele funciona como um vetor parcialmente preenchido. A função-membro getNumeroUsado retorna o número de posições de vetor utilizadas e pode, então, ser usada em um loop for, como no seguinte código-exemplo: VetorPPD algo(cap);//cap é uma variável int. for (int indice
= 0; indice < algo.getNumeroUsado( ); indice++) cout << algo[indice] << " "; objeto da classe VetorPPD tem um vetor dinâmico como variável-membro.
Um Esse vetor variável-membro armazena os elementos. A variável-membro vetor dinâmico é, na realidade, uma variável ponteiro. Em cada construtor, essa variável-membro é fixada para apontar para um vetor dinâmico. Existem também duas variáveis-membros de tipo int: a variável-membro capacidade registra o tamanho do vetor dinâmico, e a variável-membro utilizado registra o número de posições de vetor preenchidas até o momento. Como é habitual com vetores parcialmente preenchidos, os elementos devem ser preenchidos em ordem, indo primeiro para a posição 0, depois para 1, 2, e assim por diante. Um objeto da classe VetorPPD pode ser utilizado como um vetor parcialmente preenchido de doubles. Este apresenta algumas vantagens em relação a um vetor comum de doubles ou um vetor dinâmico de doubles. Diferentemente dos vetores-padrão, esse vetor emite uma mensagem de erro se um índice de vetor ilegal é utilizado. Além disso, um objeto da classe VetorPPD não requer uma variável int extra para controlar quanto do vetor foi utilizado. (Talvez você proteste: "Existe uma variável int assim. É uma variável-membro". En5.
Se você já leu a seção do Capítulo 7 sobre vectors, notará que a classe definida aqui é uma versão fraca de um vector. Mesmo que se possa utilizar um vector em qualquer lugar em que se usaria essa classe, esse exemplo continua sendo instrutivo, pois utiliza muitas das técnicas que discutimos neste capítulo. Além disso, esse exemplo dá a você uma idéia de como uma classe vector poderia ser implementada.
Classes, Ponteiros e Vetores Dinâmicos
301
tretanto, a variável-membro é uma variável-membro privada na implementação, e um programador que utilize a classe VetorPPD nunca deve precisar saber dessa variável-membro.) Um objeto da classe VetorPPD só funciona para armazenar valores de tipo double. Quando falarmos em templates, no Capítulo 16, você verá que seria fácil converter a definição em uma classe template que funcionaria para qualquer tipo, mas por enquanto vamos nos limitar a armazenar elementos de tipo double. A maioria dos detalhes da definição da classe VetorPPD utiliza apenas tópicos estudados até agora, mas existem três novos tópicos: um construtor de cópia, um destrutor e uma sobrecarga do operador de atribuição. Explicaremos o operador de atribuição sobrecarregado a seguir, e o construtor de cópia e o destrutor, nas próximas duas subseções. Para descobrir por que você iria querer sobrecarregar o operador de atribuição, suponha que a sobrecarga do operador de atribuição fosse omitida nos Painéis 10.10 e 10.11. Suponha que lista1 e lista2 fossem, então, declaradas assim: VetorPPD lista1(10), lista2(20);
Painel 10.10
Definição de uma classe com um membro vetor dinâmico
1 2 //Os objetos desta classe são vetores parcialmente preenchidos de doubles. 3 class PFArrayD 4 { 5 public: 6 PFArrayD( ); 7 //Inicializa com a capacidade de 50. 8
PFArrayD(int capacityValue);
9
PFArrayD(const PFArrayD& pfaObject);
10 11 12
void addElement(double element);
13 14
bool full( ) const { return (capacity == used); }
15
int getCapacity( ) const { return capacity; }
16
int getNumberUsed( ) const { return used; }
17 18
void emptyArray( ){ used = 0; }
19 20
double& operator[](int index);
21
PFArrayD& operator =(const PFArrayD& rightSide);
Construtor de cópia
//Pré-condição: o vetor não está cheio. //Pós-condição: o elemento foi acrescentado.
//Retorna true se o vetor estiver cheio; caso contrário, false.
//Esvazia o vetor.
//Lê e altera o acesso para os elementos de 0 até numberUsed - 1. Atribuição sobrecarregada
22 ~PFArrayD( ); 23 private: Destrutor double *a; //Para um vetor de doubles 24 int capacity; //Para o tamanho do vetor 25 26 int used; //Para o número de posições do vetor atualmente em uso 27 };
302
Ponteiros e Vetores Dinâmicos
Painel 10.11
Definições de função-membro para classe vetorPPD ( parte 1 de 2)
1 2 3 4 5
//Essas são as definições para a função-membro para a classe PFArrayD. //Elas requerem as seguintes instruções de include e using: //#include //using std::cout;
6 7 8 9
PFArrayD::PFArrayD( ) :capacity(50), used(0) { a = new double[capacity]; }
10 PFArrayD::PFArrayD(int size) :capacity(size), used(0) 11 { 12 a = new double[capacity]; 13 } 14 PFArrayD::PFArrayD(const PFArrayD& pfaObject) 15 :capacity(pfaObject.getCapacity( )), used(pfaObject.getNumberUsed( )) 16 { 17 a = new double[capacity]; 18 for (int i =0; i < used; i++) 19 a[i] = pfaObject.a[i]; 20 } 21 void PFArrayD::addElement(double element) 22 { 23 if (used >= capacity) 24 { 25 cout << "Tentativa de exceder a capacidade de PFArrayD.\n"; 26 exit(0); 27 } 28 a[used] = element; 29 used++; 30 } 31 32 double& PFArrayD::operator[](int index) 33 { 34 if (index >= used) 35 { 36 cout << "Índice ilegal em PFArrayD.\n"; 37 exit(0); 38 } 39 return a[index]; 40 } 41 PFArrayD& PFArrayD::operator =(const PFArrayD& rightSide) 42 { 43 if (capacity != rightSide.capacity) 44 { 45 delete [] a; 46 a = new double[rightSide.capacity]; 47 } 48 49 50 51
capacity = rightSide.capacity; used = rightSide.used; for (int i = 0; i < used; i++) a[i] = rightSide.a[i];
Observe que essa linha também verifica o caso de haver o mesmo objeto dos dois lados do operador de atribuição.
Classes, Ponteiros e Vetores Dinâmicos
Painel 10.11
52 53 }
Definições de função-membro para classe vetorPPD ( parte 2 de 2)
return *this;
54 PFArrayD::~PFArrayD( ) 55 { 56 delete [] a; 57 } 58
Painel 10.12
Programa-demonstração para vetorPPD ( parte 1 de 2)
1 //Programa para demonstrar a classe PFArrayD. 2 #include 3 using std::cin; 4 using std::cout; 5 using std::endl; 6 class PFArrayD 7 { 8 9 }; 10 void testPFArrayD( ); 11 //efetua um teste da classe PFArrayD.
Na Seção 11.1 do Capítulo11, mostramos como dividir esse longo arquivo nos três arquivos menores que correspondem, grosso modo, aos Painéis 10.10, 10.11 e a esse painel sem o código dos Painéis 10.10 e 10.11.
12 int main( ) 13 { 14 cout << "Este programa testa a classe PFArrayD.\n"; 15 16 17 18 19 20 21
22 23 }
24
char ans; do
{ testPFArrayD( ); cout << "Testar outra vez? (s/n) "; cin >> ans; }while ((ans == ’s’) || (ans == ’S’));
return 0;
25 void testPFArrayD( ) 26 { int cap; 27 28 cout << "Informe a capacidade deste supervetor: "; 29 cin >> cap; 30 PFArrayD temp(cap); 31 32 33
cout << "Digite até " << cap << " números não-negativos.\n"; cout << "Ponha um número negativo ao final.\n";
34
double next;
303
304
Ponteiros e Vetores Dinâmicos
Painel 10.12
Programa-demonstração para vetorPPD ( parte 2 de 2)
35 36 37 38 39 40
cin >> next; while ((next >= 0) && (!temp.full( ))) { temp.addElement(next); cin >> next; }
41 42 43 44 45 46 47 48 49 }
cout << "Você digitou os seguintes " << temp.getNumberUsed( ) << " números:\n"; int index; int count = temp.getNumberUsed( ); for (index = 0; index < count; index++) cout << temp[index] << " "; cout << endl; cout << "(mais um valor de sentinela.)\n";
DIÁLOGO PROGRAMA-USUÁRIO Este programa testa a classe PFArrayD. Informe a capacidade deste supervetor: 10 Digite até 10 números não-negativos. Ponha um número negativo ao final. 1.1 2.2 3.3 4.4 -1
Você digita os seguintes 4 números: 1.1 2.2 3.3 4.4 (mais um valor de sentinela) Testar outra vez? (s/n) n
Se lista2 recebesse uma lista de números com invocações de lista2.acrescenteElemento, então, ainda que estejamos presumindo que não haja sobrecarga do operador de atribuição, a seguinte definição de atribuição continuaria sendo definida, mas seu significado poderia não ser o que você desejaria que fosse: lista1 = lista2; Sem a sobrecarga do operador de atribuição, o operador de atribuição predefinido padrão é utilizado. Como de hábito, essa versão predefinida do operador de atribuição copia o valor de cada uma das variáveis-membros de lista2 para as correspondentes variáveis-membros da lista1. Assim, o valor de lista1.a é alterado para se tornar igual ao de lista2.a, o valor de lista1.capacidade é alterado para se tornar igual ao de lista2.capacidade e o valor de lista1.utilizado é alterado para ser igual ao de lista2.utilizado. Mas isso pode causar problemas. A variável-membro lista1.a contém um ponteiro, e a declaração de atribuição fixa esse ponteiro como igual ao valor de lista2.a. Tanto lista1.a quanto lista2.a, portanto, apontam para o mesmo lugar na memória. Assim, se você alterar o vetor lista1.a, também alterará o vetor lista2.a. De forma similar, se alterar o vetor lista2.a, também alterará o vetor lista1.a. Normalmente não é isso o que se deseja. Em geral, desejamos que o operador de atribuição produza uma cópia completamente independente do que estiver no lado direito. O modo de corrigir isso é sobrecarregar o operador de atribuição para que faça o que desejamos que faça com objetos da classe VetorPPD. Foi o que fizemos nos Painéis 10.10 e 10.11. A definição do operador de atribuição sobrecarregado no Painel 10.11 é reproduzida a seguir: VetorPPD& VetorPPD::operator =(const VetorPPD& ladoDireito) { if (capacidade != ladoDireito.capacidade) {
Classes, Ponteiros e Vetores Dinâmicos
delete []
a =
305
a;
new double [ladoDireito.capacidade];
} capacidade = ladoDireito.capacidade; utilizado = ladoDireito.utilizado; for (int i = 0; i < utilizado; i++) a[i] = ladoDireito.a[i]; return *this;
}
Quando se sobrecarrega o operador de atribuição, ele deve ser membro da classe; não pode ser amigo da classe. É por isso que a definição anterior tem apenas um parâmetro. Por exemplo: lista1 = lista2;
Na chamada anterior, lista1 é o objeto que faz a chamada e lista2, o argumento para o operador-membro =. Observe que as capacidades dos dois objetos são verificadas para se saber se são iguais. Se não forem, então a variável-membro do vetor a do lado esquerdo (ou seja, o objeto que faz a chamada) é destruída com delete e um novo vetor com a capacidade apropriada é criado por meio de new. Isso garante que o objeto do lado esquerdo do operador de atribuição terá um vetor do tamanho correto, mas tem outro efeito muito importante: garante que, se o mesmo objeto aparecer dos dois lados do operador de atribuição, o vetor nomeado pela variável-membro a não será apagado com uma chamada a delete. Para verificar por que isso é importante, considere a seguinte definição alternativa e mais simples do operador de atribuição sobrecarregado: //Esta versão contém um erro: VetorPPD& VetorPPD::operator =(const VetorPPD& ladoDireito) { delete [] a; a = new double [ladoDireito.capacidade]; capacidade = ladoDireito.capacidade; utilizado = ladoDireito.utilizado; for (int i = 0; i < utilizado; i++) a[i] = ladoDireito.a[i]; return *this;
}
Esta versão apresenta um problema quando utilizada em uma atribuição com o mesmo objeto de ambos os lados do operador de atribuição: minhaLista = minhaLista;
Quando essa atribuição é executada, o primeiro comando executado é delete []
a;
Mas o objeto que faz a chamada é minhaLista, então isso significa delete []
minhaLista.a; ponteiro minhaLista.a se
O torna indefinido. O operador de atribuição corrompeu o objeto minhaLista. Esse problema não pode acontecer com a definição do operador de atribuição sobrecarregado que apresentamos no Painel 10.11.
CÓPIA RASA E CÓPIA PROFUNDA
Quando se define um operador de atribuição sobrecarregado ou um construtor de cópia, se seu código apenas copia o conteúdo de variáveis-membros de um objeto a outro, isso é conhecido como cópia rasa. O operador de atribuição padrão e o construtor de cópia padrão executam cópias rasas. Se não existem ponteiros ou dados dinamicamente alocados envolvidos, isso funciona bem. Se alguma variável-membro nomeia um vetor dinâmico ou (aponta para alguma outra estrutura dinâmica), então em geral não se deseja uma cópia rasa. Deseja-se, em vez disso, criar uma cópia daquilo para que cada variável-membro está apontando, de forma que se obtenha uma cópia separada mas idêntica, como ilustrado no Painel 10.11. Isso se chama cópia profunda e é o que normalmente fazemos quando sobrecarregamos o operador de atribuição ou definimos um construtor de cópia.
306
Ponteiros e Vetores Dinâmicos
DESTRUTORES Variáveis dinamicamente alocadas apresentam um problema: não desaparecem a não ser que seu programa faça uma chamada adequada a delete. Mesmo que a variável dinâmica tenha sido criada com uma variável ponteiro local e esta desapareça ao final de uma chamada de função, a variável dinâmica permanecerá, a não ser que se faça uma chamada a delete. Se você não eliminar as variáveis dinâmicas com chamadas a delete, as variáveis dinâmicas continuarão a ocupar espaço na memória, o que pode fazer com que seu programa aborte, esgotando toda a memória no gerenciador da pilha. Além disso, se a variável dinâmica estiver inserida nos detalhes de implementação de uma classe, o programador que utilizar uma classe talvez não saiba sobre a variável dinâmica e, assim, não chame delete. Na realidade, como os membros dados normalmente são membros privados, o programador normalmente não pode acessar as variáveis ponteiros necessárias e, assim, não pode chamar delete com essas variáveis ponteiros. Para lidar com esse problema, o C++ possui uma função-membro especial chamada destrutor . Um destrutor é uma função-membro chamada automaticamente quando um objeto de uma classe sai fora do escopo. Se seu programa contiver uma variável local que nomeia um objeto de uma classe com um destrutor, então quando a chamada de função termina, o destrutor é chamado automaticamente. Se o destrutor estiver definido corretamente, este chamará delete para eliminar todas as variáveis dinamicamente alocadas criadas pelo objeto. Isso pode ser feito com uma única chamada a delete ou exigir várias chamadas. Além disso, talvez você queira que seu destrutor efetue mais alguns detalhes de limpeza, mas devolver memória ao gerenciador da pilha para reutilização é a principal tarefa do destrutor. A função-membro ~VetorPPD é o destrutor para a classe VetorPPD mostrada no Painel 10.10. Como um construtor, um destrutor sempre tem o mesmo nome que a classe de que é membro, mas o destrutor possui o símbolo do til, ~, no início de seu nome (dessa forma é possível distinguir um destrutor de um construtor). Como um construtor, um destrutor não tem tipo para o valor retornado, nem mesmo o tipo void. Não tem parâmetros, também. Assim, uma classe pode ter apenas um destrutor; não se pode sobrecarregar um destrutor para uma classe. Caso contrário, o destrutor seria definido como qualquer outra função-membro. Observe a definição do destrutor ~VetorPPD, fornecida no Painel 10.11. ~VetorPPD chama delete para eliminar o vetor dinamicamente alocado apontado pela variável-membro ponteiro a. Observe novamente a função-teste VetorPPD no programa mostrado no Painel 10.12. A variável local temp contém um vetor dinâmico apontado pela variável-membro temp.a. Se essa classe não possuísse um destrutor, depois que a chamada a testeVetorPPD terminasse, esse vetor dinâmico continuaria ocupando memória, ainda que vetor dinâmico não tenha mais utilidade para o programa. Além disso, cada iteração do loop do-while produziria outro vetor dinâmico inútil, que atravancaria a memória. Se o loop for iterado um número suficiente de vezes, as chamadas de função consumirão toda a memória no gerenciador da pilha e seu programa será encerrado de forma anormal. ■
DESTRUTOR
O destrutor de uma classe é uma função-membro de uma classe que é chamada automaticamente quando um objeto da classe sai do escopo. Entre outras coisas, isso significa que, se um objeto do tipo-classe é uma variável local para uma função, então o destrutor é chamado automaticamente quando a última ação antes da chamada à função se encerra. Os destrutores são usados para eliminar quaisquer variáveis dinamicamente alocadas que tenham sido criadas pelo objeto, de modo que a memória ocupada por essas variáveis dinâmicas seja devolvida ao gerenciador da pilha para reutilização. Os destrutores podem executar também outras tarefas de limpeza. O nome de um destrutor consiste no símbolo do til, ~, seguido pelo nome da classe.
CONSTRUTORES DE CÓPIA Um construtor de cópia é um construtor que possui um parâmetro do mesmo tipo que a classe. Esse parâmetro único deve ser chamado por referência e, normalmente, é precedido pelo modificador de parâmetro const, sendo, portanto, um parâmetro constante. Em todos os outros aspectos, um construtor de cópia é definido e pode ser utilizado da mesma forma que qualquer outro construtor. Por exemplo, um programa que utiliza a classe VetorPPD definida no Painel 10.10 pode conter: ■
VetorPPD b(20); for (int i = 0; i < 20; i++) b.acrescentaElemento(i);
Classes, Ponteiros e Vetores Dinâmicos
307
VetorPPD temp(b);//Inicializado pelo construtor de cópia O objeto b é inicializado com o construtor que possui um parâmetro de tipo int. De forma similar, o objeto temp é inicializado pelo construtor que possui um argumento de tipo const VetorPPD&. Quando utilizado dessa
forma, um construtor de cópia é usado como qualquer outro construtor. Um construtor de cópia deve ser definido de modo que o objeto inicializado se torne uma cópia completa, independente, de seu argumento. Assim, na declaração VetorPPD temp(b);
a variável-membro temp.a não deve ser apenas fixada com o mesmo valor que b.a; isso produziria dois ponteiros apontando para o mesmo vetor dinâmico. A definição do construtor de cópia é mostrada no Painel 10.11. Obser ve que, na definição do construtor de cópia, um novo vetor dinâmico é criado e o conteúdo de um vetor dinâmico é copiado para outro vetor dinâmico. Assim, na declaração anterior, temp é inicializado de modo que sua variávelmembro vetor seja diferente da variável-membro vetor de b. As duas variáveis-membros vetores, temp.a e b.a, contêm os mesmos valores de tipo double, mas, se uma alteração é feita em uma dessas variáveis-membros vetores, não tem efeito sobre a outra variável-membro vetor. Assim, qualquer alteração feita a temp não terá efeito sobre b. Como vimos, um construtor de cópia pode ser utilizado como qualquer outro construtor. Um construtor de cópia também é chamado automaticamente em outras situações. Grosso modo, sempre que o C++ precisa fazer uma cópia de um objeto, chama automaticamente o construtor de cópia. Em particular, o construtor de cópia é chamado automaticamente em três circunstâncias: 1. Quando um objeto classe é declarado e inicializado por outro objeto do mesmo tipo dado entre parênteses. (Este é o caso da utilização do construtor de cópia como qualquer outro construtor.) 2. Quando uma função retorna um valor do tipo-classe. 3. Sempre que um argumento do tipo-classe é "conectado" a um parâmetro chamado por valor. Nesse caso, o construtor de cópia define qual é o significado de "conectar-se". Se você não definir um construtor de cópia para uma classe, o C++ gerará automaticamente um construtor de cópia para você. Entretanto, esse construtor de cópia padrão só copia o conteúdo de variáveis-membros e não funciona corretamente para classes com ponteiros ou dados dinâmicos em suas variáveis-membros. Assim, se as suas variáveis-membros classe envolvem ponteiros, vetores dinâmicos ou outros dados dinâmicos, você deve definir um construtor de cópia para a classe. Para entender por que você precisa de um construtor de cópia, vejamos o que aconteceria se não definíssemos um construtor de cópia para a classe VetorPPD. Suponha que não tenhamos incluído o construtor de cópia na defini- ção da classe VetorPPD e que tenhamos usado um parâmetro chamado por valor em uma definição de função, por exemplo: void mostraVetorPPD(VetorPPD
parametro)
{ cout << "O primeiro valor é: " << parametro[0] << endl; }
Considere o seguinte código, que inclui uma chamada de função: VetorPPD exemplo(2); exemplo.acrescentaElemento(5.5); exemplo.acrescentaElemento(6.6); showVetorPPD(exemplo); cout << "Depois da chamada: " << exemplo[0] << endl;
Como nenhum construtor de cópia foi definido para a classe VetorPPD, a classe possui um construtor de cópia padrão que apenas copia o conteúdo das variáveis-membros. Eis o que ocorre: quando a chamada de função é executada, o valor de exemplo é copiado para a variável local parametro, então parametro.a é fixado como igual a exemplo.a. Mas essas são variáveis ponteiros, assim durante a chamada de função parametro.a e exemplo.a apontam para o mesmo vetor dinâmico, da seguinte forma:
308
Ponteiros e Vetores Dinâmicos
Quando a chamada de função termina, o destrutor para VetorPPD é chamado para devolver a memória utilizada por parametro ao gerenciador da pilha de modo que seja reutilizada. A definição do destrutor contém o seguinte comando: delete []
a;
Como o destrutor é chamado com o objeto delete []
parametro,
esse comando é equivalente a
parametro.a;
que altera a figura para a seguinte:
Como exemplo.a e parametro.a apontam para o mesmo vetor dinâmico, apagar parametro.a é o mesmo que apagar exemplo.a. Assim, exemplo.a é indefinido quando o programa chega ao comando cout << " Depois da chamada:
" << exemplo[0] << endl;
então esse comando cout é indefinido. O comando cout pode, por acaso, fornecer a saída que você deseja; no entanto, mais cedo ou mais tarde, o fato de exemplo.a ser indefinido causará problemas. Um grave problema ocorre quando o objeto exemplo é uma variável local em alguma função. Nesse caso, o destrutor será chamado com exemplo quando a chamada de função terminar. Esse destrutor será equivalente a delete []
exemplo.a;
Mas, como acabamos de ver, o vetor dinâmico apontado por exemplo.a já foi apagado uma vez, e agora o sistema está tentando apagá-lo uma segunda vez. Chamar delete duas vezes para apagar o mesmo vetor dinâmico (ou qualquer outra variável criada com new) pode causar um grave erro de sistema que pode levar o programa a travar. É o que aconteceria se não houvesse construtor de cópia. Felizmente, incluímos um construtor de cópia em nossa definição da classe VetorPPD, e o construtor de cópia é chamado automaticamente quando a seguinte chamada de função é executada: mostraVetorPPD(exemplo);
O construtor de cópia define o que significa conectar o argumento valor parametro, de modo que agora a figura é assim:
exemplo para
o parâmetro chamado por
Assim, qualquer alteração feita a parametro.a não tem efeito sobre o argumento exemplo e, portanto, não há problemas com o destrutor. Se o destrutor é chamado para parametro e, depois, para exemplo, cada chamada ao destrutor apaga um vetor dinâmico diferente.
Classes, Ponteiros e Vetores Dinâmicos
309
Quando uma função retorna um valor de um tipo-classe, o construtor de cópia é chamado automaticamente para copiar o valor especificado pelo comando return. Se não houver construtor de cópia, problemas similares aos que descrevemos para os parâmetros chamados por valor ocorrerão. Se uma definição de classe envolve ponteiros e memória dinamicamente alocada utilizando o operador new, é necessário incluir um construtor de cópia. Classes que não envolvem ponteiros ou memória alocada dinamicamente não precisam definir um construtor de cópia. Ao contrário do que se poderia esperar, o construtor de cópia não é chamado quando se fixa um objeto como igual a outro com o operador de atribuição. 6 Entretanto, se você não gosta do que o operador de atribuição padrão faz, pode redefini-lo, como fizemos nos Painéis 10.10 e 10.11. CONSTRUTOR DE CÓPIA Um construtor de cópia é um construtor que possui um parâmetro chamado por referência do mesmo tipo que a classe. Esse parâmetro único deve ser chamado por referência; normalmente, o parâmetro também é constante — ou seja, é antecedido pelo modificador de parâmetros const. O construtor de cópia para uma classe é chamado automaticamente sempre que uma função retorna um valor do tipo-classe. O construtor de cópia também é chamado automaticamente sempre que um argumento é conectado a um parâmetro chamado por valor do tipo-classe. Um construtor de cópia também pode ser utilizado da mesma forma que os outros construtores. Qualquer classe que utilize ponteiros e o operador new deve ter um construtor de cópia.
O GRANDE TRIO O construtor de cópia, o operador de atribuição = e o destrutor são chamados de "o grande trio", porque os especialistas dizem que, se você precisar de qualquer um deles, precisará dos três. Se algum deles estiver faltando, o compilador criará, mas o item criado pode não se comportar da forma que você deseja. Assim, é melhor você mesmo os definir. O construtor de cópia e o operador de atribuição sobrecarregado = que o compilador gera funcionarão bem se todas as variáveis-membros forem de tipos predefinidos, como int e double. Para qualquer classe que utilizar ponteiros e o operador new, é mais seguro definir seu próprio construtor de cópia, = sobrecarregado e um destrutor.
13. Se uma classe é nomeada MinhaClasse e possui um construtor, como o constructor é nomeado? Se MinhaClasse possuir um destrutor, como ele será nomeado? 14. Suponha que você altere a definição do destrutor no Painel 10.11 para a seguinte. Como ficaria o diálogo programa-usuário no Painel 10.12? VetorPPD::~VetorPPD( ) { cout << "\nAdeus mundo cruel! A vida curta deste\n" << "vetor dinâmico está prestes a se encerrar.\n"; delete [] a; }
15. A linha seguinte é a primeira da definição do construtor de cópia para a classe VetorPPD. O identificador VetorPPD ocorre três vezes e significa algo diferente a cada vez. O que significa em cada um dos três casos? VetorPPD::VetorPPD(const VetorPPD& pfaObjeto)
16. Responda a essas perguntas sobre destrutores: a. O que é um destrutor e como deve ser seu nome? b. Quando um destrutor é chamado? c. O que um destrutor faz, de fato? d. O que um destrutor deveria fazer?
6.
O C++ faz distinção entre inicialização (os três casos em que o construtor de cópia é chamado) e atribuição. A inicialização utiliza o construtor de cópia para criar um novo objeto; o operador de atribuição toma um objeto existente e o modifica de modo que o torne uma cópia idêntica (em todos os aspectos, exceto posição) do lado direito da atribuição.
310
Ponteiros e Vetores Dinâmicos
17. a. Explique cuidadosamente por que não é necessário nenhum operador de atribuição sobrecarregado quando os únicos dados são tipos internos. b. O mesmo que o item a para um construtor de cópia. c. O mesmo que o item a para um destrutor.
■
■
■
■
■
■
■
Um ponteiro é um endereço de memória e, assim, proporciona um meio de se nomear indiretamente uma variável nomeando o endereço da variável na memória do computador. Variáveis dinâmicas (também chamadas variáveis dinamicamente alocadas) são variáveis criadas (e destruídas) enquanto um programa é executado. A memória para variáveis dinâmicas fica em um local especial da memória do computador, chamada gerenciador de pilhas. Quando um programa se encerra com uma variável dinâmica, a memória utilizada pela variável dinâmica pode ser devolvida ao gerenciador da pilha para reutilização; isso é feito com um comando delete. Um vetor dinamicamente alocado (também chamado apenas de vetor dinâmico) é um vetor cujo tamanho é determinado quando o programa é executado. Um vetor dinâmico é implementado como uma variável dinâmica de um tipo vetor. Um destrutor é uma função-membro especial para uma classe. Um destrutor é chamado automaticamente quando um objeto da classe sair do escopo. A principal razão da existência dos destrutores é devolverem a memória para o gerenciador da pilha para ser reutilizada. Um construtor de cópia é um construtor que possui um único argumento do mesmo tipo da classe. Se você definir um construtor de cópia, ele será chamado automaticamente sempre que uma função retornar um valor de tipo-classe e sempre que um argumento for conectado a um parâmetro chamado por valor do tipo-classe. Qualquer classe que utilize ponteiros e o operador new deve ter um construtor de cópia. Quando se sobrecarrega o operador de atribuição, ele deve ser sobrecarregado como um operador-membro. Não se esqueça de verificar se sua sobrecarga funciona quando a mesma variável está em ambos os lados do operador de atribuição sobrecarregado.
RESPOSTAS DOS EXERCÍCIOS DE AUTOTESTE 1. Um ponteiro é o endereço de memória de uma variável. 2. int *p; // Declara um ponteiro para uma variável int. *p = 17; //Aqui, * é o operador de desreferenciação. Isso atribui //17 à posição de memória apontada por p. void func(int* p); // Declara p como um valor ponteiro // parametro. 3. 10 20 20 20 30 30 Se você substituir *p1 = 30; por *p2 = 30;, a saída será a mesma. 4. 10 20 20 20 30 20
5. Para o descuidado ou para o iniciante, parecem dois objetos de tipo ponteiro para int, ou seja, int*. Infelizmente, o * se liga ao identificador, não ao tipo (ou seja, não ao int). O resultado é que essa declaração declara intPtr1 como um ponteiro int, enquanto intPtr2 é uma variável int comum. 6. delete p; 7. typedef double * NumeroPtr; NumeroPtr meuPonto;
Projetos de Programação
311
8. O operador new requer um tipo como argumento. new aloca espaço na pilha para uma variável do tipo do argumento. Retorna um ponteiro para aquela memória, desde que haja espaço suficiente. Se não houver espaço suficiente, o operador new pode apresentar como saída NULL, ou abortar o programa, dependendo de como seu compilador funciona. 9. typedef char * CharVetor; 10. cout << "Digite 10 inteiros:\n"; for (int i = 0; i < 10; i++)
cin >> entrada[i]; 11. delete [] entrada; 12. 0 1 2 3 4 5 6 7 8 9
13. O construtor é nomeado MinhaClasse, o mesmo nome da classe. O destrutor é nomeado ~MinhaClasse. 14. O diálogo ficaria assim: Este programa testa a classe VetorPPD. Informe a capacidade deste supervetor: 10 Digite até 10 números não-negativos. Coloque um número negativo ao final. 1.1 2.2 3.3 4.4 -1 Você digitou os seguintes 4 números: 1.1 2.2 3.3 4.4 (mais um valor sentinela.) Adeus mundo cruel! A vida curta deste vetor dinâmico está prestes a se encerrar. Testar outra vez? (s/n) n 15. O VetorPPD antes do :: é o nome da classe. O VetorPPD após o :: é o nome da função-membro. (Lembre-se de que um construtor é uma função-membro que possui o mesmo nome que a classe.) O VetorPPD entre parênteses é o tipo para o parâmetro pfaObjeto.
16. a. Um destrutor é uma função-membro de uma classe. O nome de um destrutor sempre começa com um til, ~, seguido pelo nome da classe. b. Um destrutor é chamado quando um objeto classe sai do escopo. c. Um destrutor, na realidade, faz o que o autor da classe o programou para fazer! d. Supõe-se que um destrutor apague variáveis dinâmicas que foram alocadas por construtores para a classe. Os destrutores também podem executar outras tarefas de limpeza. 17. No caso do operador de atribuição = e do construtor de cópia, se houver apenas tipos internos como dados, o mecanismo de cópia é exatamente o que você deseja, de modo que o padrão funciona bem. No caso do destrutor, nenhuma alocação dinâmica de memória é feita (nada de ponteiros), então a ação do padrão (de não fazer nada) também é a que você deseja.
PROJETOS DE PROGRAMAÇÃO 1. Releia o código no Painel 10.9. Em seguida, reescreva uma classe DoisD que implemente o vetor dinâmico bidimensional de doubles empregando idéias deste painel em seus construtores. Você deve ter um membro privado de tipo ponteiro para double que aponte para o vetor dinâmico e dois valores int (ou unsigned int), MaxLinhas e MaxCols. Inclua um construtor-padrão para o qual você deve escolher tamanhos máximos de linhas e colunas e um construtor parametrizado que permita ao programador fixar o tamanho máximo de linhas e colunas. Além disso, inclua uma função-membro void que permita estabelecer um registro em uma linha e coluna particulares e uma função-membro que retorne uma entrada de uma linha e coluna particulares como um valor de tipo double.
312
Ponteiros e Vetores Dinâmicos
Observação: é difícil ou impossível (dependendo dos detalhes) sobrecarregar [ ] para que funcione como você gostaria para vetores bidimensionais. Desse modo, utilize apenas funções de acesso e mutantes empregando a notação de função habitual. Sobrecarregue o operador + como uma função amiga para acrescentar dois vetores bidimensionais. Essa função deve retornar o objeto DoisD cujo elemento da iésima linha, jésima coluna é a soma do elemento da iésima linha, jésima coluna do operando do lado esquerdo do objeto DoisD com o elemento da iésima linha, jésima coluna do operando do lado direito do objeto DoisD. Providencie um construtor de cópia, um operador= sobrecarregado e um destrutor. Declare funções-membros classe que não alterem os dados como membros const. 2. Utilizando vetores dinâmicos, implemente uma classe polinomial com adição, subtração e multiplicação de polinômios. Discussão: uma variável em um polinômio não faz nada além de atuar como um "guardador" de lugar para os coeficientes. Assim, só o que interessa nos polinômios é o vetor de coeficientes e expoente correspondente. Pense no polinômio x*x*x + x + 1
Onde está o termo em x*x? Um modo simples de implementar a classe polinomial é utilizar um vetor de doubles para armazenar os coeficientes. O índice do vetor é o expoente do termo correspondente. Se há um termo faltando, então ele possui coeficiente zero. Existem técnicas para representar polinômios de graus elevados com muitos termos faltando. São as chamadas técnicas de matriz esparsa. A não ser que você já conheça essas técnicas, ou aprenda muito rápido, não as utilize. Forneça um construtor-padrão, um construtor de cópia e um construtor parametrizado que possibilite que um polinômio arbitrário seja construído. Inclua um operador sobrecarregado = e um destrutor. Forneça essas operações: polinômio + polinômio, constante + polinômio, polinômio + constante, polinômio - polinômio, constante - polinômio, polinômio - constante. polinômio * polinômio, constante * polinômio, polinômio * constante, Inclua funções para atribuir e extrair coeficientes, indexadas por expoente. Inclua uma função para calcular o polinômio como um valor de tipo double. Decida entre implementar essas funções como membros, amigas ou independentes.
Compilação Separada e Namespaces Compilação Separada e Namespaces
Capítulo 11Compilação Separada e Namespaces De minha própria biblioteca, com volumes que eu prezo mais do que meu ducado. William Shakespeare, A Tempestade
INTRODUÇÃO Este capítulo aborda dois tópicos relacionados aos meios de organizar um programa em C++ em partes separadas. A Seção 11.1, sobre compilação separada, discute como um programa em C++ pode ser distribuído em diversos arquivos, de modo que, quando alguma parte do programa tem de ser alterada, apenas essa parte precise ser recompilada e as partes separadas possam ser mais facilmente reutilizadas em outras aplicações. A Seção 11.2 discute namespaces, que apresentamos brevemente no Capítulo 1. Namespaces são um meio de permitir a reutilização de nomes de classes, funções e outros itens, qualificando os nomes para indicar usos diferentes. Os namespaces dividem seu código em seções, para que diferentes seções possam reutilizar os mesmos nomes com diferentes significados. Eles permitem uma espécie de significação local para nomes que é mais geral que as variáveis locais. Este capítulo pode ser lido antes da ordem em que está no livro. Não utiliza nenhum material dos Capítulos 5 (vetores), 9 (strings), 10 (ponteiro e vetores dinâmicos) nem da Seção 7.3 (vectors) do Capítulo 7.
11.1
Compilação Separada O seu "se" é o único pacificador; muita virtude em "se". William Shakespeare, Como Gostais
O C++ possui recursos para dividir um programa em partes mantidas em arquivos separados, compilados separadamente e depois ligados quando o programa é executado (ou logo antes de ser executado). Pode-se colocar a definição para uma classe (e suas definições de função associadas) em arquivos separados dos programas que utilizam a classe. Dessa forma, pode-se construir uma biblioteca de classes de modo que vários programas utilizem a mesma classe. Pode-se compilar a classe uma vez e depois utilizá-la em vários programas diferentes, do mesmo modo como se usam as bibliotecas predefinidas como aquelas com os arquivos de cabeçalho iostream e cstdlib. Além disso, pode-se definir a própria classe em dois arquivos, de modo que a especificação do que a classe faz é separada de como a classe é implementada. Se você alterar apenas a implementação da classe, vai precisar recompilar apenas o arquivo contendo a implementação da classe. Os outros arquivos, inclusive os que contêm os programas que utilizam a classe, não precisam ser alterados ou mesmo recompilados. Esta seção diz a você como efetuar essa compilação separada de classes.
314 ■
Compilação Separada e Namespaces
ENCAPSULAMENTO — REVISÃO
O princípio do encapsulamento diz que se deve separar a especificação de como a classe é utilizada por um programador dos detalhes de como a classe é implementada. A separação deve ser tão completa que se possa alterar a implementação sem ser necessário alterar qualquer programa que utilize a classe. O modo de se assegurar essa separação pode ser resumido em três regras: 1. Tornar todas as variáveis-membros privadas membros da classe. 2. Tornar cada uma das operações básicas para a classe uma função-membro pública da classe, uma função amiga, uma função comum ou um operador sobrecarregado. Agrupar a definição de classe e as definições de função e operador (protótipos). Esse grupo, com os comentários que o acompanham, é chamado de interface da classe. Especifique como utilizar cada função ou operador em um comentário dado com a classe ou com a declaração de função ou operador. 3. Torne a implementação das operações básicas indisponível ao programador que utiliza a classe. A implementação consiste nas definições da função e do operador sobrecarregado (com qualquer função de ajuda ou outros itens requeridos por essas definições). Em C++, a melhor forma de assegurar que se sigam essas regras é colocar a interface e a implementação da classe em arquivos separados. Como você pode imaginar, o arquivo que contém a interface em geral é chamado de arquivo de interface, e o arquivo que contém a implementação, arquivo de implementação. Os detalhes exatos de como montar, compilar e utilizar esses arquivos variam levemente de uma versão de C++ para outra, mas o esquema básico é o mesmo em todas as versões. Em particular, os detalhes do conteúdo desses arquivos são os mesmos em todos os sistemas. Variam somente os comandos utilizados para compilar e ligar ( link ) esses arquivos. Os detalhes do conteúdo desses arquivos são ilustrados na próxima subseção. Uma classe típica tem variáveis-membros privadas. Variáveis-membros privadas (e funções-membros privadas) apresentam um problema para nossa filosofia básica de colocar a interface e a implementação de uma classe em arquivos separados. A parte pública da definição de uma classe faz parte da interface da classe, mas a parte privada faz parte da implementação. Isso é um problema, porque o C++ não permite que você divida a definição da classe em dois arquivos. Assim, algum tipo de compromisso é necessário. O único compromisso sensato é colocar toda a definição da classe no arquivo de interface. Como um programador que esteja utilizando a classe não pode utilizar qualquer dos membros privados da classe, os membros privados continuarão, na realidade, ocultos do programador. ■
ARQUIVOS DE CABEÇALHO E ARQUIVOS DE IMPLEMENTAÇÃO
O Painel 11.1 contém o arquivo de interface de uma classe chamada HoraDigital. HoraDigital é uma classe cujos valores são horas do dia, como 9:30. Somente os membros públicos da classe fazem parte da interface. Os membros privados fazem parte da implementação, embora estejam no arquivo de interface. O rótulo private: avisa que esses membros privados não fazem parte da interface pública. Tudo o que um programador precisa saber a fim de utilizar a classe HoraDigital é explicado no comentário no início do arquivo e nos comentários na seção pública da definição da classe. Como foi observado no comentário no início do arquivo de interface, essa classe utiliza notação de 24 horas; assim, por exemplo, 1:30 da tarde é transmitida na entrada e na saída como 13:30. Estes e outros detalhes que você precisa conhecer, a fim de utilizar bem a classe HoraDigital, estão incluídos nos comentários fornecidos com as funções-membros. Colocamos a interface em um arquivo chamado horad.h. O sufixo .h indica que se trata de um arquivo de cabeçalho. Um arquivo de interface é sempre um arquivo de cabeçalho e, assim, sempre termina com o sufixo .h. Qualquer programa que utilize a classe HoraDigital deve conter uma instrução de include como a seguinte, que dá nome a esse arquivo: #include "horad.h"
Quando se escreve uma instrução de include, deve-se indicar se o arquivo de cabeçalho é um arquivo de cabeçalho predefinido fornecido a você ou um arquivo de cabeçalho que você escreveu. Se for predefinido, escreva o nome do arquivo de cabeçalho entre parênteses angulares, como . Se o arquivo de cabeçalho tiver sido escrito por você, escreva o nome do arquivo de cabeçalho entre aspas, como " horad.h". Essa distinção diz ao compilador onde procurar o arquivo de cabeçalho. Se o nome do arquivo de cabeçalho estiver entre parênteses angulares, o compilador procura no lugar em que os arquivos de cabeçalhos predefinidos são guardados na sua im-
Compilação Separada
315
plementação de C++. Se estiver entre aspas, o compilador procura no diretório atual ou no lugar em que os arqui vos de cabeçalhos definidos pelo programador são mantidos em seu sistema. Qualquer programa que utilizar a nossa classe HoraDigital deve conter a instrução de include apresentada, que dá ao arquivo de cabeçalho o nome horad.h. Isso basta para permitir que seu programa seja compilado, mas não basta para que você possa executar o programa. Para executar o programa você precisa escrever (e compilar) as definições das funções-membros e dos operadores sobrecarregados. Colocamos essas definições de função e do operador em outro arquivo, chamado de arquivo de implementação . Embora isso não seja exigido pela maioria dos compiladores, é costume dar ao arquivo de interface e ao arquivo de implementação o mesmo nome. Na realidade, os dois arquivos terminam com sufixos diferentes. Colocamos a interface de nossa classe no arquivo chamado horad.h e a implementação da classe em um arquivo chamado horad.cpp. O sufixo que se utiliza para o arquivo de implementação depende da sua versão de C++. Utilize para o arquivo de implementação o mesmo sufixo usado normalmente para arquivos que contêm programas em C++. (Outros sufixos comuns são .cxx e .hxx.) O arquivo de implementação para a nossa classe HoraDigital é dado no Painel 11.2. Depois que explicarmos como os vários arquivos de nossa classe interagem uns com os outros, voltaremos ao Painel 11.2 e discutiremos os detalhes das definições neste arquivo de implementação. Painel 11.1
1 2 3 4 5
Arquivo de interface para a classe HoraDigital (parte 1 de 2)
//Este é o arquivo de cabeçalho dtime.h. Esta é a interface para a classe DigitalTime. //Valores deste tipo são referentes ao dia. Tanto na entrada quanto na saída os valores //são em notação de 24 horas, como em 9:30 para 9:30 AM e 14:45 para 2:45 PM. #include using namespace std;
6 class DigitalTime 7 { 8 public: 9 DigitalTime(int theHour, int theMinute); 10 DigitalTime( ); 11 //Inicializa o valor do tempo como 0:00 (meia-noite). 12 13 14 15
getHour( ) const; getMinute( ) const; void advance(int minutesAdded); //Altera o tempo para minutesAdded minutos depois.
16 17
void advance(int hoursAdded, int minutesAdded);
18 19
friend bool operator ==(const DigitalTime& time1, const DigitalTime& time2);
20
friend istream& operator >>(istream& ins, DigitalTime& theObject);
21 22
friend ostream& operator <<(ostream& outs, const DigitalTime& theObject); private:
//Altera o tempo para hoursAdded mais minutesAdded minutos depois.
23 24
int hour; int minute;
25 26 27 28 29
static void readHour(int& theHour);
30 31
static void readMinute(int& theMinute);
Essas variáveis-membros e funções de ajuda fazem parte da implementação. Não fazem parte da interface. A palavra private indica que elas não fazem parte da interface pública.
//Pré-condição: A próxima entrada a ser lida a partir do teclado é um tempo em //notação, como 9:45 ou 14:45. //Pós-condição: TheHour foi fixada como a parte horária do tempo. //Os dois-pontos foram descartados, e a próxima entrada a ser lida é a dos minutos.
//Lê os minutos a partir do teclado depois que readHour leu a hora.
316
Compilação Separada e Namespaces
Painel 11.1
Arquivo de interface para a classe HoraDigital (parte 2 de 2)
32 static int digitToInt(char c); 33 //Pré-condição: c é um dos dígitos de ‘0‘ a ‘9‘. 34 //Retorna o inteiro para o dígito; por exemplo, digitToInt(‘3‘) retorna 3. 35 36 };
Painel 11.2
Arquivo de implementação ( parte 1 de 3)
1 2 3 4 5 6 7
/Este é o arquivo de implementação dtime.cpp da classe DigitalTime. //A interface para a classe DigitalTime está no arquivo de cabeçalho dtime.h. #include #include #include using namespace std; #include "dtime.h"
8 9 10 11 12 13 14 15 16 17 18 19 20
//Utiliza iostream e cstdlib: DigitalTime::DigitalTime(int theHour, int theMinute) { if (theHour < 0 || theHour > 24 || theMinute < 0 || theMinute > 59) { cout << "Argumento ilegal para o construtor DigitalTime."; exit(1); }
else
{ hour = theHour; minute = theMinute; }
21 22 23 }
if (hour == 24)
hour = 0; //Padroniza meia-noite como 0:00
24 DigitalTime::DigitalTime( ) 25 { 26 hour = 0; 27 minute = 0; 28 } 29 30 31 32 33 34 35 36 37
int DigitalTime::getHour( ) const
{
return hour;
} int DigitalTime::getMinute( ) const
{
return minute;
}
38 void DigitalTime::advance(int minutesAdded) 39 { 40 int grossMinutes = minute + minutesAdded; 41 minute = grossMinutes%60; 42 int hourAdjustment = grossMinutes/60; 43 hour = (hour + hourAdjustment)%24;
Compilação Separada Painel 11.2
Arquivo de implementação ( parte 2 de 3)
44 } 45 void DigitalTime::advance(int hoursAdded, int minutesAdded) 46 { 47 hour = (hour + hoursAdded)%24; 48 advance(minutesAdded); 49 } 50 bool operator ==(const DigitalTime& time1, const DigitalTime& time2) 51 { 52 return (time1.hour == time2.hour && time1.minute == time2.minute); 53 } 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
//Utiliza iostream: ostream& operator <<(ostream& outs, const DigitalTime& theObject) { outs << theObject.hour << ’:’; if (theObject.minute < 10) outs << ’0’; outs << theObject.minute; return outs; }
71 72 73 74
int DigitalTime::digitToInt(char c)
//Utiliza iostream: istream& operator >>(istream& ins, DigitalTime& theObject) { DigitalTime::readHour(theObject.hour); DigitalTime::readMinute(theObject.minute); return ins; } {
return ( int(c) - int(’0’) );
}
75 //Utiliza iostream, cctype e cstdlib: 76 void DigitalTime::readMinute(int& theMinute) 77 { 78 char c1, c2; 79 cin >> c1 >> c2; 80 81 82 83 84 } 85
if (!(isdigit(c1) && isdigit(c2)))
86 87 88 89 90 91 92 93 94 95 96 97
if (theMinute < 0 || theMinute > 59)
{ cout << "Erro: entrada ilegal para readMinute \n"; exit(1); theMinute = digitToInt(c1)*10 + digitToInt(c2); { cout << "Erro: entrada ilegal para readMinute\n"; exit(1); }
} //Utiliza iostream, cctype e cstdlib: void DigitalTime::readHour(int& theHour) { char c1, c2; cin >> c1 >> c2;
317
318
Compilação Separada e Namespaces
Painel 11.2
Arquivo de implementação ( parte 3 de 3)
98 99 100 101 102
if (
!( isdigit(c1) && (isdigit(c2) || c2 == ’:’ ) ) )
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117
if (isdigit(c1)
{ cout << "Erro: entrada ilegal para readHour\n"; exit(1); } && c2 == ’:’)
{ theHour = DigitalTime::digitToInt(c1); } else
//(isdigit(c1) && isdigit(c2))
{ theHour = DigitalTime::digitToInt(c1)*10 + DigitalTime::digitToInt(c2); cin >> c2;//discard ’:’ if (c2 != ’:’) { cout << "Erro: entrada ilegal para redHour\n"; exit(1); } }
118 119
if (theHour
== 24) theHour = 0; //Padroniza meia-noite como 0:00
120 121 122 123 124 125 }
if (
theHour < 0 || theHour > 23 )
{ cout << "Erro: entrada ilegal para readHour\n"; exit(1); }
Qualquer arquivo que utilizar a classe HoraDigital deve conter a instrução de include #include "horad.h"
Assim, tanto o arquivo de implementação quanto o arquivo de programa devem conter a instrução de include que dá nome ao arquivo de interface. O arquivo que contém o programa (ou seja, o arquivo que contém a função main) em geral é chamado de arquivo de aplicação ou arquivo driver . O Painel 11.3 contém um arquivo de aplicação com um programa bastante simples que utiliza e demonstra a classe HoraDigital. Os detalhes exatos de como executar esse programa completo, contido em três arquivos, dependem do sistema que você utiliza. Entretanto, os detalhes básicos são iguais para todos os sistemas. Você deve compilar o arquivo de implementação e o arquivo de aplicação que contém a função main. Não compile o arquivo de interface, que nesse exemplo é o arquivo horad.h, fornecido no Painel 11.1. Não é preciso compilar o arquivo de interface porque o compilador pensa que o conteúdo desse arquivo de interface já está inserido em cada um dos outros dois arquivos. Lembre-se de que tanto o arquivo de implementação quanto o arquivo de aplicação contêm a instrução #include "horad.h"
Compilar o programa invoca automaticamente um pré-processador que lê essa instrução de include e a substitui pelo texto no arquivo horad.h. Assim, o compilador vê o conteúdo de horad.h, e o arquivo horad.h não precisa ser compilado separadamente. (Na realidade, o compilador vê o conteúdo de horad.h duas vezes: uma quando se compila o arquivo de implementação e outra quando se compila o arquivo de aplicação.) Essa cópia do arquivo horad.h é apenas uma cópia conceitual. O compilador age como se o conteúdo de horad.h fosse copiado em cada arquivo que possui a instrução de include. Entretanto, se você procurar nesses arquivos depois que forem compilados, encontrará apenas a instrução de include; não o conteúdo do arquivo horad.h.
Compilação Separada
319
Uma vez que o arquivo de implementação e o arquivo de aplicação tenham sido compilados, você precisa ainda conectar esses arquivos para que possam trabalhar juntos. Isso se chama fazer o linking dos arquivos e é feito por um utilitário separado chamado linker . Os detalhes de como chamar o linker dependem de qual sistema é utilizado. Muitas vezes, o comando para executar um programa invoca automaticamente o linker, e você não precisa chamálo explicitamente. Depois que os arquivos estiverem conectados, você pode executar o programa. Talvez esse processo pareça complicado, mas muitos sistemas possuem recursos que lidam com vários desses detalhes para você, automática ou semi-automaticamente. Em qualquer sistema, os detalhes logo se tornam rotina. Em sistemas UNIX, esses detalhes são gerenciados por meio de make. Na maioria dos IDEs (Integrated Develop- ment Environments — Ambientes de Desenvolvimento Integrado), esses vários arquivos são combinados em algo que se chama projeto. Painel 11.3
Arquivo de aplicação utilizando a classe HoraDigital
1 2 3 4
//Este é o arquivo de aplicação timedemo.cpp que demonstra o uso de DigitalTime. #include using namespace std; #include "dtime.h"
5 6 7
int main( )
{ DigitalTime clock, oldClock;
8 9 10 11
cout << "Você pode digitar meia-noite como 0:00 ou 24:00,\n" << "mas eu sempre escreverei 0:00.\n" << "Digite a hora em notação de 24 horas: "; cin >> clock;
12 13 14 15 16 17 18
oldClock = clock; clock.advance(15); if (clock == oldClock) cout << "Algo está errado."; cout << "Você digitou " << oldClock << endl; cout << "15 minutos depois a hora será " << clock << endl;
19 20 21 22
clock.advance(2, 15); cout << "2 horas e 15 minutos depois disso\n" << "a hora será " << clock << endl;
23 24 }
return 0;
DIÁLOGO PROGRAMA-USUÁRIO Você pode digitar meia-noite como 0:00 ou 24:00, mas eu sempre escreverei 0:00. Digite a hora em notação de 24 horas: 11:15 Você digitou 11:15 15 minutos depois a hora será 11:30 2 horas e 15 minutos depois disso a hora será 13:45
320
Compilação Separada e Namespaces
DEFININDO UMA CLASSE EM ARQUIVOS SEPARADOS: RESUMO
Pode-se definir uma classe e colocar sua definição e implementação de suas funções-membros em arquivos separados. Em seguida, pode-se compilar a classe separadamente de qualquer programa que a utilize e usá-la em quantos programas diferentes se desejar. A classe é colocada em arquivos da seguinte forma: 1. Coloque a definição da classe em um arquivo de cabeçalho chamado arquivo de interface . O nome desse arquivo de cabeçalho tem a extensão .h. O arquivo de interface também contém as declarações (protótipos) para quaisquer funções e operadores sobrecarregados que definam operações de classe básicas, mas que não estejam listados na definição de classe. Inclua comentários que expliquem como todas essas funções e operadores são utilizados. 2. As definições de todas as funções e operadores sobrecarregados mencionadas anteriormente (quer sejam membros, amigas ou nenhum dos dois) são colocadas em outro arquivo chamado arquivo de implementação. Esse arquivo deve conter uma instrução de include que dê nome ao arquivo de interface descrito antes. Essa instrução de include vem entre aspas, como no seguinte exemplo: #include "dtime.h"
Os arquivos de interface e de implementação costumam ter o mesmo nome, mas terminam com sufixos diferentes. O arqui vo de interface termina em .h. O de implementação, com o mesmo sufixo utilizado para os arquivos que contêm um programa completo em C++. O arquivo de implementação é compilado separadamente antes de ser utilizado em qualquer programa. 3. Quando se deseja utilizar a classe em um programa, deve-se colocar a parte main do programa (e quaisquer definições de função, declarações de constant e outras) em outro arquivo, chamado arquivo de aplicação ou arquivo driver. Esse arquivo também deve conter uma instrução de include que dê nome ao arquivo de interface, como no seguinte exemplo: #include "dtime.h"
O arquivo de aplicação é compilado separadamente do arquivo de implementação. Pode-se escrever quantos desses arquivos de aplicação utilizar com um par de arquivos de interface e de implementação. Para executar um programa inteiro, é preciso primeiro linkar o código objeto produzido compilando o arquivo de aplicação e o código objeto produzido compilando o arquivo de implementação. (Em alguns sistemas o linking deve ser feito automaticamente ou semi-automaticamente.) Caso se utilizem classes múltiplas em um programa, basta ter múltiplos arquivos de interface e de implementação, cada um compilado separadamente.
Os Painéis 11.1, 11.2 e 11.3 contêm um programa completo dividido em pedaços e colocado em três arqui vos diferentes. Você poderia combinar o conteúdo desses três arquivos em um arquivo e, então, compilar e executar esse arquivo, sem toda essa confusão de instruções de include e de conectar arquivos separados. Por que se preocupar em fazer três arquivos separados? Existem diversas vantagens em se dividir o programa em arquivos separados. Quando se tem a definição e a implementação da classe HoraDigital em arquivos separados do arquivo de aplicação, pode-se utilizar essa classe em muitos programas diferentes sem precisar reescrever a definição da classe em cada programa. Além disso, você precisa compilar o arquivo de implementação apenas uma vez, independentemente de quantos programas utilizem a classe HoraDigital. Mas existem outras vantagens além destas. Quando se tem a interface separada da implementação da sua classe HoraDigital, pode-se alterar o arquivo de implementação sem precisar alterar qualquer programa que utilize a classe. Na realidade, não é necessário sequer recompilar o programa. Se você alterar o arquivo de implementação, só precisa recompilar o arquivo de implementação e reconectar os arquivos. Economizar tempo de recompilação é bom, mas a maior vantagem é evitar a necessidade de reescrever o código. Você pode utilizar a classe em vários programas sem escrever o código da classe em cada um deles. Você pode alterar a implementação da classe e não precisar reescrever nenhuma parte do programa que a utiliza. Os detalhes da implementação da classe HoraDigital serão discutidos seção "Exemplo" a seguir. Classe HoraDigital
Já descrevemos como os arquivos nos Painéis 11.1, 11.2 e 11.3 dividem um programa em três arquivos: a interface para a classe HoraDigital , a implementação da classe HoraDigital e uma aplicação que utiliza a classe. Agora vamos tratar dos detalhes da implementação da classe. Não há conteúdo novo nesta seção de exemplo, mas se alguns dos detalhes sobre a implementação (Painel 11.2) não estiverem totalmente claros para você, esta seção pode lançar alguma luz na confusão em que você se encontra. A maioria dos detalhes de implementação é fácil, mas há duas questões que merecem comentário. Observe que o nome da função-membro adiantar foi sobrecarregado para que tivesse duas definições de função. Observe também que a definição do operador de extração (entrada) sobrecarregado >> utiliza duas funções de ajuda chamadas leHora e leMinuto e que essas funções de ajuda utilizam uma terceira função de ajuda chamada digitoParaInt . Vamos discutir essas questões.
Compilação Separada
321
A classe HoraDigital (Painéis 11.1 e 11.2) possui duas funções-membros chamadas adiantar. Uma versão requer um único argumento, ou seja, um inteiro dando o número de minutos para se adiantar a hora. A outra versão requer dois argumentos, um para o número de horas e outro para o número de minutos, e adianta a hora daquele número de horas mais aquele número de minutos. Observe que a definição da versão de adiantar de dois argumentos inclui uma chamada à versão de um argumento. Olhe para a definição da versão de dois argumentos dada no Painel 11.2. Primeiro a hora é adiantada de horasAcrescentadas horas e depois a versão de argumento único de adiantar é utilizada para adiantar a hora de mais minutosAcrescentados minutos. A princípio isso pode parecer estranho, mas é perfeitamente válido. As duas funções chamadas adiantar são diferentes e que, no que se refere ao compilador, apenas por coincidência possuem o mesmo nome. Agora vamos falar das funções de ajuda. As funções de ajuda leHora e leMinuto lêem a entrada de um caractere de cada vez e, depois, convertem a entrada em valores inteiros, que são colocados nas variáveismembros hora e minuto. As funções leHora e leMinuto lêem a hora e o minuto, um dígito de cada vez, portanto são valores de leitura de tipo char. Isso é mais complicado do que ler a entrada como valores int, mas permite que executemos verificações de erro para ver se a entrada está correta e emitir uma mensagem de erro se não estiver. Essas funções de ajuda leHora e leMinuto utilizam outra função de ajuda chamada digitoParaInt . A função digitoParaInt converte um dígito, como ’3’, em um número, como 3. Essa função foi introduzida anteriomente neste livro pela resposta ao Exercício de Autoteste 3, no Capítulo 7.
COMPONENTES REUTILIZÁVEIS Uma classe desenvolvida e codificada em arquivos separados é um componente de software que pode ser reutilizado inúmeras vezes em diferentes programas. Um componente reutilizável economiza esforço, porque não precisa ser reprojetado, recodificado e retestado para cada aplicação. Um componente reutilizável tende também a ser mais confiável que um utilizado apenas uma vez, por duas razões: primeiro, porque se pode gastar mais tempo e esforço em um componente se ele for utilizado várias vezes; segundo, porque, se o componente é utilizado várias vezes, é testado várias vezes. Cada vez que se utiliza um componente de software há um teste deste componente. Utilizar um componente de software várias vezes em diversos contextos é uma das melhores formas de se descobrir se existem erros remanescentes no software.
■ UTILIZANDO #ifndef
Nós lhe fornecemos um método para colocar um programa em três (ou mais) arquivos: dois para a interface e a implementação de cada classe e um para a parte de aplicação do programa. Um programa pode ser composto por mais de três arquivos. Por exemplo, um programa pode utilizar diversas classes, e cada classe pode ser mantida em um par separado de arquivos. Suponha que você tenha um programa espalhado em diversos arquivos e que mais de um arquivo possua uma instrução de include para um arquivo de interface para uma classe como a seguinte: #include "horad.h"
Sob essas circunstâncias, pode-se ter arquivos que incluam outros arquivos, e esses outros arquivos podem incluir, por sua vez, ainda outros. Isso pode conduzir facilmente a uma situação em que um arquivo contenha as definições de horad.h mais de uma vez. O C++ não permite que se defina uma classe mais de uma vez, mesmo se as definições repetidas forem idênticas. Além disso, se você estiver utilizando o mesmo arquivo de cabeçalho em muitos projetos diferentes, é quase impossível manter o controle e saber se você incluiu uma definição de classe mais de uma vez. Para evitar esse problema, o C++ fornece um modo de se assinalar uma seção de código com o significado "se você já incluiu esse texto antes, não o inclua de novo". Isso é feito de uma maneira bastante intuitiva, embora a notação possa parecer estranha até você se acostumar com ela. Vamos explicar com o auxílio de um exemplo. A seguinte instrução define HORAD_H: #define HORAD_H
Isso significa que o pré-processador do compilador coloca HORAD_H em uma lista para indicar que HORAD_H foi vista. Definida talvez não seja a melhor palavra no caso, já que HORAD_H não foi definida como nada, mas apenas incluída em uma lista. O importante é que se pode utilizar outra instrução para testar se HORAD_H foi definida e,
322
Compilação Separada e Namespaces
assim, testar se uma seção de código já foi processada. Pode-se utilizar qualquer identificador (que não seja pala vra-chave) em lugar de HORAD_H, mas você verá que existem convenções-padrão quanto ao identificador que se deve utilizar. A seguinte instrução testa se HORAD_H foi definida: #ifndef HORAD_H
Se HORAD_H já tiver sido definida, tudo entre essa instrução e a primeira ocorrência da seguinte instrução é ignorado: #endif
Uma maneira equivalente de se dizer isso, que pode esclarecer o modo como se escrevem as instruções, é a seguinte: se HORAD_H não tiver sido definida, o compilador processa tudo até o próximo #endif. É por causa desse não que existe um n em #ifndef. (Talvez você se pergunte se existe uma instrução #ifdef, além da #ifndef. Existe, e possui o significado óbvio, mas não teremos oportunidade de utilizar #ifdef.) Agora considere o seguinte código: #ifndef HORAD_H #define HORAD_H #endif