2  Conceitos básicos de programação em C++

Um programa, como se viu no capítulo anterior, consiste na concretização prática dum algoritmo numa dada linguagem de programação.  Um programa em C++ consiste num conjunto de instruções (sem qualquer tipo de ambiguidade) que são executadas duma forma sequencial.  As instruções: Neste capítulo apresentar-se-á um conjunto de instruções básicas existentes em todos os programas e que incluem formas de definir variáveis e expressões que utilizam ou modificam o seu valor.

2.1  Estrutura básica dum programa em C++

Um programa em C++ tem normalmente uma estrutura semelhante à seguinte (todas as linha precedidas de // são comentários, sendo portanto ignorados pelo compilador, servindo para documentar os programas, i.e., explicar ou clarificar as intenções do programador):
// As duas linhas seguintes são necessárias para permitir a
// apresentação de variáveis no ecrã e a inserção de valores
// através do teclado (usando os canais [ou streams] cin e cout):
#include <iostream>
using namespace std;

// A linha seguinte indica o ponto onde o programa vai começa a ser executado.
// Indica também que o programa não usa argumentos e que o valor por ele
// devolvido ao sistema operativo é um inteiro (estes assuntos serão vistos em
// pormenor em aulas posteriores):
int main()
{   // esta chaveta assinala o início do programa.

    // Aqui aparecem as instruções que compõem o programa.

}   // esta chaveta assinala o fim do programa.

A primeira linha, #include ..., serve para obter as declarações do canal de leitura de dados do teclado (cin) e do canal de escrita de dados no ecrã (cout).

A linha seguinte, using namespace std;,  é  uma directiva de utilização do espaço nominativo std, e serve para se poder escrever simplesmente cout em vez de std::cout.  O seu uso não é, em geral, recomendável [1, pág.171], sendo usado aqui apenas para simplificar a apresentação sem tornar os programas inválidos (muitas vezes estas duas linhas iniciais não serão incluídas nos exemplos, devendo o leitor estar atento a esse facto se resolver compilar esses exemplos).  Os espaços nominativos serão abordados na disciplina de Programação Orientada para Objectos.

Quanto a main, não passa duma função que tem a particularidade de ser a primeira a ser invocada no programa.  As funções e procedimentos serão vistos em pormenor no Capítulo 3.

2.2  Variáveis

2.2.1  Memória e inicialização

Os computadores têm memória, sendo através da sua manipulação que os programas eventualmente acabam por chegar aos resultados pretendidos (as saídas).  As linguagens de alto nível, como o C++, "escondem" a memória por trás do conceito de variável.  As variáveis são, na realidade, pedaços de memória a que se atribui um nome, que têm um determinado conteúdo ou valor, e cujo conteúdo é interpretado de acordo com o tipo da variável.  Todas as variáveis têm um dado tipo.  Uma variável pode ser, por exemplo, do tipo int em C++.  Se assim for, essa variável terá sempre um valor inteiro de uma gama de valores admissível.

Os tipos das variáveis não passam, na realidade, de uma abstração.  Todas as variáveis, independentemente do seu tipo, são representadas na memória do computador por padrões de bits (dígitos binários, binary digit), os famosos "zeros e uns", colocados na zona de memória atribuída a essa variável.  O tipo duma variável indica simplesmente como um dado padrão de bits deve ser interpretado.

Cada variável tem duas características estáticas: um nome e um tipo, e uma característica dinâmica: um valor.  Antes de usar uma variável é necessário indicar ao compilador qual o seu nome e tipo, de modo a que a variável possa ser criada, i.e., ficar associada a uma posição de memória (ou várias posições de memória), e de modo a que a forma de interpretar os padrões de bits nessa posição de memória fique estabelecida.  Uma instrução onde se cria uma variável com um dado nome, dum determinado tipo, e com um determinado valor inicial denomina-se definição.  A instrução:

int a = 10;
é a definição de uma variável chamada a que pode guardar valores do tipo int (inteiros) e cujo valor inicial é o inteiro 10.  A sintaxe das definições pode ser algo complicada, mas em geral tem a forma acima, isto é, o nome do tipo seguido do nome da variável e seguido de uma inicialização.

Uma forma intuitiva de ver uma variável é imaginá-la como uma folha de papel com um nome associado e onde se decidiu escrever apenas números inteiros, por exemplo.  Outras restrições são que a folha de papel pode conter apenas um valor em cada instante, pelo que a escrita dum novo valor implica o apagamento do anterior, e que tem de conter sempre um valor, como se viesse já preenchida de fábrica.

Quando uma variável é definida, o computador reserva para ela na memória o número de bits necessário para guardar um valor do tipo referido (essas reservas são feitas em múltiplos de uma unidade básica de memória, tipicamente com oito bits, ou seja, um byte).  Se a variável não for explicitamente inicializada, essa posição de memória contém um padrão de bits arbitrário.  Por exemplo, se se tivesse usado a definição

int a;
a variável a conteria um padrão de bits arbitrário e portanto um valor inteiro arbitrário.  Para evitar esta arbitrariedade, que pode ter consequências nefastas num programa se não se tiver cuidado, ao definir uma variável deve-se, sempre que possível e razoável, atribuir-se-lhe um valor inicial como indicado na primeira definição.  A atribuição de um valor a uma variável que acabou de ser definida é chamada a inicialização.  Esta operação pode ser feita de várias maneiras *:
int a = 10;  // como originalmente.
int a(10);   // forma alternativa.
A sintaxe das definições de variáveis também permite que se definam mais do que uma variável numa só instrução.  Por exemplo,
int a = 0, b = 1, c = 2;
define três variáveis todas do tipo int.

* Na realidade as duas formas não são rigorosamente equivalentes (ver nas Curiosidades).

2.2.2  Nomes de variáveis

Os nomes de variáveis (e em geral de todos os identificadores em C++) podem ser constituídos por letras (sendo as minúsculas distinguidas das maiúsculas), dígitos decimais, e também pelo caractere '_' (sublinhado ou underscore), que se usa normalmente para aumentar a legibilidade do nome.  O nome de uma variável não pode conter espaços nem pode começar por um dígito.  Durante a tradução do programa, o compilador, antes de fazer uma análise sintáctica do programa, faz uma análise lexical.  Esta análise é "gulosa": os símbolos detectados (palavras, sinais de pontuação, etc.) são tão grandes quanto possível, pelo que lêValor é sempre interpretado como um único identificador (nome), e não por seguido de Valor.

Os nomes das variáveis devem ser tão auto-explicativos quanto possível.  Se uma variável guarda, por exemplo, o número de alunos numa turma, deve chamar-se número_de_alunos_na_turma ou então número_de_alunos, se o complemento "na turma" for óbvio pelo contexto do programa.  É claro que o nome usado para uma variável não tem qualquer importância para o compilador, mas uma escolha apropriada dos nomes pode aumentar grandemente a legibilidade dos programas.

2.2.3  Tipos básicos do C++

O tipo das variáveis indica quantos bits tem a sua representação na memória do computador e a forma como os padrões de bits guardados em variáveis desse tipo devem ser interpretados.  A linguagem C++ tem definidos a priori alguns tipos: os chamados tipos básicos do C++.  Em aulas posteriores estudar-se-ão formas de acrescentar à linguagem novos tipos mais apropriados para o problema em causa: a isso se chama programação baseada em objectos (programação orientada para objectos é outro conceito e será estudado na disciplina de Programação Orientada para Objectos).

Nas tabelas seguintes são apresentados os tipos básicos existentes no C++ e a gama de valores que podem representar no Visual C++.  É muito importante notar que os computadores são máquinas finitas: tudo é limitado, desde a memória ao tamanho da representação dos tipos em memória.  Assim, a gama de diferentes valores possíveis de guardar em variáveis de qualquer tipo é limitada.

A gama de valores representável para cada tipo de dados pode variar com o processador e o sistema operativo.  Por exemplo, no sistema operativo Linux em processadores Alpha, os long int (segunda tabela) têm 64 bits.  Em geral não se pode afirmar muito mais do que: a gama dos long é sempre suficiente para abarcar qualquer int e a gama dos int é sempre suficiente para abarcar qualquer short, o mesmo acontecendo com os long double relativamente aos double e com os double relativamente aos float.

Ambas as tabelas se referem ao compilador de C++ g++ da GNU Compiler Collection (GCC 2.91.66, egcs 1.1.2) num sistema Linux, distribuição Redhat 6.0, núcleo 2.2.5, correndo sobre a arquitectura Intel, localizado para a Europa Ocidental.
 

Tipos básicos elementares
Tipo Descrição Gama Bits
bool valor booleano true e false 8
int número inteiro -231 a 231-1 
(-2147483648 a 2147483647)
32
float número decimal 
(com vírgula)
1,17549435 × 10-38
3,40282347 × 1038 (e negativos)
32
char caractere 
(usa o código ISO-Latin-1)
Qualquer caractere.  Exemplos: 
'a', 'A', '1', '!', '*', etc.
8

 
 
Outros tipos básicos
Tipo Descrição Gama Bits
short [int] número inteiro -215 a 215-1 (-32768 a 32767) 16
unsigned short [int] número inteiro positivo 0 a 216-1 (0 a 65535) 16
unsigned [int] número inteiro positivo 0 a 232-1 (0 a 4294967295) 32
long [int] número inteiro a mesma que int 32
unsigned long [int] número inteiro positivo a mesma que unsigned int 32
double número decimal  2,2250738585072014 × 10-308
1,7976931348623157 × 10+308 (e negativos)
64
long double número decimal 3.36210314311209350626e × 10-4932 a 1.18973149535723176502 × 104932
(e negativos)
96

Alguns dos tipos derivados do int podem ser escritos duma forma abreviada: os [] na tabela indicam a parte opcional na especificação do tipo.

O qualificador signed também pode ser usado, mas signed int e int são sinónimos.  O único caso em que este qualificador faz diferença é na construção signed char, mas apenas em máquinas onde os char não têm sinal.

A representação interna dos vários tipos pode ser ou não relevante para os programas.  A maior parte das vezes não é relevante, excepto quanto ao facto de que se deve sempre estar ciente das limitações de qualquer tipo.  Por vezes, no entanto, a representação é muito relevante, nomeadamente quando se programa ao nível do sistema operativo, que muitas vezes tem de aceder a bits individuais.  Assim, apresentam-se em seguida algumas noções sobre a representação usual dos tipos básicos do C++.  Esta matéria será pormenorizada em Arquitectura de Computadores.

Tipos aritméticos

Os tipos aritméticos são todos os tipos que permitem representar números (int, float e seus derivados).  Variáveis (e valores literais, ver Secção 2.3) destes tipos podem ser usados para realizar operações aritméticas e relacionais, que serão vistas nas próximas secções.  Os tipos derivados de int chamam-se tipos inteiros.  Os tipos derivados de float chamam-se tipos de vírgula flutuante.  Os char, em rigor, também são tipos aritméticos e inteiros, mas serão tratados à parte.
Representação de inteiros
Admita-se para simplificar, que, num computador hipotético, as variáveis do tipo unsigned int têm 4 bits (normalmente têm 32 bits).  Podemos representar esquematicamente uma dada variável do tipo unsigned int como:
 
b3
b2
b1
b0

em que os bi com i = 0, ..., 3 são bits, tomando portanto os valores 0 ou 1.  É fácil verificar que existem apenas 24 = 16 padrões diferentes de bits possíveis de colocar numa destas variáveis.  Como associar valores inteiros a cada um desses padrões?  A resposta mais óbvia é a que é usada na prática: considere-se que o valor representado é (b3b2b1b0)2, i.e., que os bits são dígitos dum número expresso na base binária, ou seja, na base 2 (recorda-se que o termo bit significa binary digit).  Por exemplo:
 
1
0
0
1

é o padrão de bits correspondente ao valor (1001)2, ou seja 9 em decimal.

Suponha-se agora que a variável contém
 
1
1
1
1

e que se soma 1 ao seu conteúdo: o resultado é (1111 + 1)2 = (10000)2.  Mas este valor não é representável num unsigned int de quatro bits!  Um dos bits tem de ser descartado.  O que acontece é que são guardados apenas os 4 bits menos significativos do resultado pelo que, no computador hipotético em que os inteiros têm 4 bits, (1111 + 1)2 = (0000)2.  Ou seja, nesta aritmética binária com um número limitado de dígitos, tudo funciona como se os valores possíveis estivessem organizados em torno de um relógio, neste caso em torno de um relógio com 16 horas, onde após as (1111)2 = 15 horas são de novo (0000)2 = 0 horas *.

A extensão destas ideias para, por exemplo, 32 bits é trivial: nesse caso o (grande) relógio teria 232 horas, de 0 a 232 - 1, que é, de facto, a gama dos unsigned int no Linux com a configuração apresentada.

É extremamente importante recordar as limitações dos tipos.  Em particular os valores das variáveis do tipo int não podem crescer indefinidamente.  Ao se atingir o topo do relógio volta-se a zero!

Falta verificar como representar inteiros com sinal, i.e., incluindo não só valores positivos mas também valores negativos.  Suponha-se que os int têm, de novo num computador hipotético, apenas 4 bits.  Admita-se uma representação semelhante à dos unsigned int, mas diga-se que, no lugar das 15 horas (padrão de bits 1111), mesmo antes de chegar de novo ao padrão 0000, o valor representado é x.  Pelo que se disse anteriormente, somar 1 a x corresponde a rodar o ponteiro do padrão 1111 para o padrão 0000.  Admitindo que o padrão 0000 representa de facto o valor 0, tem-se x + 1 = 0, donde se conclui ser o padrão 1111 um bom candidato para representar o valor -1!  Estendendo o argumento anterior, pode-se dizer que as 14 horas correspondem à representação de -2, e assim sucessivamente.  Assim,
 

Padrão de bits na variável
Valor representado
0000
0
0001
1
0010
2
0011
3
0100
4
0101
5
0110
6
0111
7
1000
-8
1001
-7
1010
-6
1011
-5
1100
-4
1101
-3
1110
-2
1111
-1

O "salto" nos valores no relógio deixa de ocorrer do padrão 1111 para o padrão 0000 (15 para 0 horas, se interpretados como inteiros sem sinal), para passar a ocorrer na passagem do padrão 0111 para o padrão 1000 (das 7 para as -8 horas, se interpretados como inteiros com sinal).  A escolha deste local para a transição não foi arbitrária.  Em primeiro lugar permite representar um número semelhante de valores positivos e negativos: 7 positivos e 8 negativos.  Deslocando a transição de uma hora no sentido dos ponteiros do relógio, poder-se-ia alternativamente representar 8 positivos e 7 negativos.  A razão para a escolha representada na tabela acima prende-se com o facto de que, dessa forma, a distinção entre não negativos (positivos ou zero) e negativos se pode fazer olhando apenas para o bit mais significativo (o bit mais à esquerda), que quando é 1 indica que o valor representado é negativo.  A esse bit chama-se, por isso, bit de sinal.  Esta representação chama-se representação em complemento para 2.  Em Arquitectura de Computadores as vantagens desta representação para simplificação do hardware do computador encarregue de fazer operações com valores inteiros ficarão claras.

Como saber, olhando para um padrão de bits, qual o valor representado?  Primeiro olha-se para o bit de sinal.  Se for 0, interpreta-se o padrão de bits como um número binário: é esse o valor representado.  Se o bit de sinal for 1, então o valor representado é igual ao valor binário correspondente ao padrão de bits subtraído de 16.  Por exemplo, observando o padrão 1011, conclui-se que representa o valor negativo (1011)2 - 16 = -5, como se pode confirmar na tabela acima.

A extensão destas ideias para o caso dos int com 32 bits é muito simples.  Neste caso os valores representados variam de -231 a 231 - 1 e a interpretação dos valores representados faz-se como indicado no parágrafo acima, só que subtraíndo 232 no caso dos valores negativos.  Por exemplo, os padrões 00000000000000000000010000000001 e 10000000000000000000000000000111 representam os valores 1025 e -2147483641, como se pode verificar facilmente com uma máquina de calcular.

* De acordo, tipicamente no topo do relógio estaria 16, e não zero, mas optaremos pela numeração de 0 a 15.

Representação de valores de vírgula flutuante
Os tipos de vírgula flutuante destinam-se a representar valores decimais, i.e., "com vírgula" ou parte "decimal".  Porventura a forma mais simples de representar valores com vírgula passa por usar exactamente a mesma representação que para os inteiros (com sinal), mas admitir que todos os valores devem ser divididos por uma potência fixa de 2.  Por exemplo, admitindo que se usam 8 bits e a divisão feita é por 16 = 24 = (10000)2, temos que o padrão 01001100 representa o valor (01001100)2 / (1000)2 = (0100,1100)2 = 4,75.

Esta representação corresponde a admitir que os bits representam um valor binário em que a vírgula se encontra já não logo após o bit menos significativo (o bit mais à direita), como nos inteiros, mas quatro posições para a esquerda.  O menor valor positivo representável é 1/16 = 0,0625.  Por outro lado, só se conseguem representar valores de -8 a -0,0625, 0, e 0,0625 a 7,9375.  O número de dígitos decimais de precisão está portanto entre 2 e 3.  Caso se utilizem 32 bits e vírgula 16 posições para a esquerda, o menor valor positivo representável é 1/216 ±= 0,00001526, ou seja, são representáveis valores de -32768 a -0,00001526, 0, e 0,00001526 a 32767,99998, aproximadamente, correspondendo a cerca de 10 dígitos decimais de precisão, cinco antes da vírgula e cinco depois.

A este tipo de representação chama-se vírgula fixa, por se colocar a vírgula numa posição fixa.

A representação em vírgula fixa impõe limitações consideráveis na gama de valores representáveis: no caso dos 32 bits com vírgula 16 posições para a esquerda, representam-se valores (em módulo) apenas entre aproximadamente 10-5 e 105.  Na prática este problema resolve-se usando uma representação em que a vírgula não está fixa, mas varia consoante as necessidades: a vírgula passa a "flutuar".  Uma vez que especificar a posição da vírgula é o mesmo que especificar uma potência de dois pela qual o valor deve ser multiplicado, a representação de vírgula flutuante corresponde a especificar um número da forma m × 2e, em que a m se chama mantissa e a e expoente.  Aliás, na prática os valores são especificados na forma s × m × 2e, em que m é sempre não negativo e s é o sinal, valendo ou -1 ou 1.

A representação prática de valores de vírgula flutuante passa pois por dividir o padrão de bits disponível em três zonas, com representações diferentes: o sinal, a mantissa e o expoente.  Como parte dos bits têm de ser transferidos para o expoente, o que se ganha na gama de valores representáveis perde-se na precisão dos valores.  Por exemplo, na maior parte dos processadores utilizados hoje em dia usa-se uma das representações especificadas na norma IEEE 754 [2] que, no caso de valores representados em 32 bits (i.e., de precisão simples, correspondentes ao tipo float do C++),

  1. atribui 1 bit (s0) ao sinal (0 significa positivo e 1 negativo),
  2. atribui 23 bits (m22...m0) à mantissa, que são interpretados como um valor de vírgula fixa sem sinal com vírgula deslocada de 23 posições para a esquerda, e
  3. atribui 8 bits (e7...e0) ao expoente, que são interpretados como um inteiro entre 0 e 255 a que se subtrai 127 (os valores 0 e 255 são especiais, pelo que o expoente varia entre -126 e 127);
ou seja,
 
s0
e7
e6
e5
e4
e3
e2
e1
e0
m22
m21
m20
m19
m18
m17
m16
m15
m14
m13
m12
m11
m10
m9
m8
m7
m6
m5
m4
m3
m2
m1
m0

Os valores representados são dados pela tabela que se segue:
 

s0 (sinal)
(m22...m0)2 (mantissa)
(e7...e0)2 (expoente)
valor representado
s = 1 se s0 = 0 
s = -1 se s0 = 1
0
0
0
s = 1 se s0 = 0 
s = -1 se s0 = 1
diferente de 0 
m = (m22...m0)2 × 2-23
0
valores não normalizados*
s × m × 2-126
s = 1 se s0 = 0 
s = -1 se s0 = 1
m = 1 + (m22...m0)2 × 2-23
= (1,m22...m0)2
1 a 254 
e = (e7...e0)2 - 127
valores normalizados*
s × m × 2e
s = 1 se s0 = 0 
s = -1 se s0 = 1
0
255
s × infinito
s = 1 se s0 = 0 
s = -1 se s0 = 1
diferente de 0
255
valores especiais

Note-se que, quando os bits do expoente não são todos 0 nem todos 1 (i.e., (e7...e0)2 não é 0 nem 255), a mantissa tem um bit adicional à esquerda, implícito, com valor sempre 1.  Nesse caso diz-se que os valores estão normalizados.

É fácil verificar que o menor valor positivo normalizado representável é 1 × 2-126 ±= 1,17549435 × 10-38 e o maior é (224 - 1) × 2-23 × 2127 ±= 3,40282347 × 1038 (conferir com a tabela das gamas dos tipos básicos do C++, tipo float).  Com esta representação conseguem-se cerca de seis dígitos decimais de precisão, menos quatro que a representação de vírgula fixa apresentada em primeiro lugar, mas uma gama bastante maior de valores representáveis.

A representação de valores de vírgula flutuante e pormenores de implementação de operações com essas representações serão estudados com maior pormenor na disciplina de Arquitectura de Computadores.

No que diz respeito à programação, a utilização de valores de vírgula flutuante deve ser evitada sempre que possível: se for possível usar inteiros nos cálculos é preferível usá-los a recorrer aos tipos de vírgula flutuante, que, apesar da sua aparência inocente, reservam muitas surpresas.  Em particular é importante recordar que os valores são representados com precisão finita, o que implica arredondamentos e acumulações de erros.  Outra fonte comum de erros prende-se com a conversão de valores na base decimal para os formatos de vírgula flutuante em base binária: valores inocentes como 0.4 não são representáveis exactamente (experimente escrevê-lo em base 2)!  A forma de lidar com estes tipos de erros sem surpresas é estudada pela disciplina de análise numérica [3].

* A representação diz-se normalizada se o seu bit mais significativo for 1.  A versão normalizada dos valores em formato IEEE 754 tem sempre o bit mais significativo, que é implícito, a 1.  Os valores não normalizados têm, naturalmente, menor precisão (dígitos significativos) que os normalizados.

Booleanos ou lógicos

Consistem num único tipo, bool, que guarda valores lógicos.  A forma de representação usual tem 8 bits e reserva o padrão 00000000 para representar o valor falso (false), todos os outros representando o valor verdadeiro (true).  Os valores booleanos podem ser convertidos em inteiros.  Nesse caso o valor false é convertido em 0 e o valor true é convertido em 1.  Por exemplo,
int i = int(true);
inicializa a variável i com o valor inteiro 1.

Caracteres

O tipo char é tratado de um modo especial.  Em cada variável do tipo char é armazenado o código de um caractere.  Esse código consiste num padrão de bits, correspondendo cada padrão de bits a um determinado caractere.  Cada padrão de bits pode também ser interpretado como um número inteiro em binário, das formas que se viram atrás.  Assim, em C++, cada caracter é representado por um determinado valor inteiro, o seu código, correspondente a um determinado padrão de bits.

Os char são tipicamente representados usando 8 bits, pelo que existem 256 diferentes caracteres representáveis.  Existem várias tabelas de codificação de caracteres diferentes.  De longe a mais neste canto da Europa é a tabela dos códigos ISO-Latin-1 (ou melhor, ISO-8859-1), que estende a tabela ASCII (American Standard Code for Information Interchange) de modo a incluir os caracteres acentuados em uso na Europa Ocidental.  Existem muitas destas extensões, que podem ser usadas de acordo com os caracteres que se pretendem, i.e., de acordo com o alfabeto da língua utilizada.  Essas extensões são possíveis porque os códigos ASCII usam apenas os 7 bits menos significativos dum caracter (i.e., apenas 128 das possíveis combinações de zeros e uns) *.

Não é necessário, em C++, saber os códigos dos caracteres: para indicar o código do caractere 'b' usa-se 'b', que tem o mesmo significado que 98 (em ISO-Latin-1).  O que de facto é armazenado numa variável do tipo char quando lhe é atribuido o valor 'b' é a representação binária do número 98.  Isso permite escrever em linguagem C++ sem preocupações com a tabela de caracteres em uso.

É possível tratar um char como um pequeno número inteiro.  Por exemplo, se se executar o conjunto de instruções seguinte:

char caractere = 'i';
++caractere; // o mesmo que caractere = caractere + 1;
cout << "O caracter seguinte é: " << caractere << endl;
aparece no ecrã:
O caracter seguinte é: j
O que sucedeu foi que se adicionou 1 ao código do caracter 'i', de modo que a variável caractere passou a conter o código do caractere 'j'.  Este pedaço de código só é garantidamente válido se se souber que, na tabela que usada (no caso é ISO-Latin-1), o alfabeto possui códigos sequenciais.  É importante realizar que isso nem sempre acontece.   Por exemplo, na tabela EBCDIC (Extended Binary Coded Decimal Interchange Code), o caractere 'i' tem código 137 e o caractere 'j' tem código 145!  Num sistema que use esse código as instruções acima não escreveriam a letra 'j' no ecrã.

O programa seguinte imprime no ecrã todos os caracteres da tabela ASCII (que só especifica os caracteres correspondentes aos códigos de 0 a 127, isto é, todos os valores positivos dos char em Linux e a primeira metade da tabela ISO-Latin-1) *:

#include <iostream>
using namespace std;

int main(void)
{
    for(int i = 0; i < 128; ++i) {
        cout << "'" << char(i) << "' (" << i << ")" << endl;
    }
}
Os caracteres (char) são interpretados como inteiros em C++.  Quer em Linux sobre processadores Intel quer no Windows NT, a gama dos códigos dos caracteres vai de -128 a 127, isto é, os char são interpretados como inteiros com sinal.  Pode-se usar o tipo unsigned char para obter uma gama de 0 a 255.

* Alguns dos caracteres escritos são especiais, representando mudanças de linha, etc.  Por isso, o resultado de uma impressão no ecrã de todos os caracteres do código ASCII pode ter alguns efeitos estranhos.

* Existe um outro tipo  de codificação, o Unicode, suporta todos os caracteres de todas as expressões escritas vivas ou mortas em simultâneo, embora exija uma codificação diferente, com maior número de bits.

2.2.4  Inicialização de variáveis

As posições de memória correspondentes às variáveis contêm sempre um padrão de bits qualquer: não há posições "vazias".  Assim, as variáveis têm sempre um valor qualquer.  Depois de uma definição, e se não for feita uma inicialização explícita conforme sugerido mais atrás, a variável definida contém um valor arbitrário (desde que seja de um tipo básico do C++).  Uma fonte frequente de erros é o esquecimento de inicializar as variáveis definidas.  Por isso, é recomendável a inicialização de todas as variáveis tão cedo quanto possível, desde que para isso não seja necessário inicializá-las com valores sem qualquer significado para o resto do programa.

Existem algumas variáveis de tipos básicos do C++ que são inicializadas implicitamente com zero (ver secção sobre Permanência de variáveis).  Evite fazer uso dessa característica do C++: inicialize-as sempre explicitamente.

2.3  Valores literais

Valores literais permitem-nos indicar explicitamente valores dos tipos básicos do C++ num programa.  Exemplos de valores literais  (note bem a utilização dos sufixos U, L e F):
'a'     // do tipo char, representa o código do caractere 'a'.
100     // do tipo int, valor 100 (em decimal).
100U    // do tipo unsigned.
100L    // do tipo long.
100.0   // do tipo double.
100.0F  // do tipo float (e não double).
100.0L  // do tipo long double.
1.1e230 // do tipo double, valor 1,1 × 10230.
Os valores inteiros podem ainda ser especificados em octal (base 8) ou hexadecimal (base 16).  Inteiros precedidos de 0x são considerados como representados na base hexadecimal e portanto podem incluir, para além dos 10 dígitos usuais, as letras entre A a F, em maiúsculas ou em minúsculas.  Inteiros precedidos de 0 são considerados como representados na base octal e portanto podem incluir apenas dígitos entre 0 e 7.  Por exemplo:
0x1U   // o mesmo que 1U.
0x10FU // o mesmo que 271U, ou seja (00000000000000000000000100001111)2.
077U   // o mesmo que 63U, ou seja (00000000000000000000000000111111)2.
(Os exemplos assumem que os int têm 32 bits.)

2.4  Constantes

Nos programas em C++ também se pode definir constantes, i.e. "variáveis" que não mudam de valor durante todo o programa.  Nas constantes o valor é uma característica estática e não dinâmica como nas variáveis.  A definição de constantes faz-se usando a palavra-chave const.  As constantes, justamente por o serem, têm obrigatoriamente de ser inicializadas no acto da definição, i.e., o seu valor estático tem de ser indicado na própria definição.  Por exemplo:
const int primeiro_primo = 2;
const char primeira_letra_do_alfabeto_latino = 'a';
As constantes devem ser usadas como alternativa aos valores literais quando estes tiverem uma semântica (um significado) particular.  O nome dado à constante deve reflectir esse significado.  Por exemplo, em vez de
double raio = 3.14;
cout << "O perímetro é " << 2.0 * 3.14 * raio << endl;
cout << "A área é " <<  3.14 * raio * raio << endl;
é preferível
const double pi = 3.14;
double raio = 3.14;
cout << "O perímetro é " << 2 * pi * raio << endl;
cout << "A área é " <<  pi * raio * raio << endl;
Há várias razões para ser preferível a utilização de constantes no código acima:
  1. A constante tem um nome apropriado (bem conhecido), o que torna o código C++ mais legível.
  2. Se se pretender aumentar a precisão do valor usado para pi, basta alterar a inicialização da constante:
    1. const double pi = 3.1415927;
      double raio = 3.14;
      cout << "O perímetro é " << 2 * pi * raio << endl;
      cout << "A área é " <<  pi * raio * raio << endl;
  3. Se não se usar a constante e se pretender aumentar a precisão do valor usado, pode-se ter a tentação de usar a facilidade de substituição dos editores de texto, correndo-se o sério risco de substituir também o valor inicial da variável raio, cujo valor é, por coincidência, 3,14, resultando no código C++ errado:
    1. const double pi = 3.1415927;
      double raio = 3.1415927;  // erro! substituição desastrosa!
      cout << "O perímetro é " << 2 * pi * raio << endl;
      cout << "A área é " <<  pi * raio * raio << endl;

2.5  Expressões e operadores

Em C++, o tipo de instrução mais simples após a instrução de definição de variáveis consiste numa expressão terminada por ponto-e-vírgula.  Assim, as próximas são instruções válidas, se bem que inúteis:
; // expressão nula (instrução nula).
2;
1 + 2 * 3;
Para que os programas tenham algum interesse é necessário que "algo mude" à medida que são executados, i.e., que o estado da memória (ou de dispositivos associados ao computador, como o ecrã, uma impressora, etc.) seja alterado.  Em C++ isso consegue-se, por exemplo, alterando os valores das variáveis através do operador de atribuição.  Note-se que, ao contrário do que se passa noutras linguagens (como o Pascal), a atribuição não é uma instrução, é uma operação.  As próximas instruções já parecem mais úteis, pois agem sobre o valor de uma variável:
int i;
i = 2;
i = 1 + 2 * 3;
Uma expressão é composta por variáveis e operações.  Normalmente numa expressão existe um operador de atribuição (=, ou variantes), excepto quando a expressão for argumento de alguma função ou quando controlar alguma instrução de selecção ou iteração (a ver em aulas futuras).  Por exemplo:
a = b + 3;
Esta expressão significa "atribua-se à variável a o resultado da soma do valor da variável b com o valor inteiro (do tipo int) literal 3".

Outro exemplo:

int i, j;
bool i_é_igual_a_j;
// ...
i_é_igual_a_j = i == j;
Esta expressão significa "atribua-se à variável i_é_igual_a_j o valor lógico (booleano, do tipo bool) da comparação entre os valores (inteiros) das variáveis a e b".  Depois desta operação o valor de i_é_igual_a_j é V se a for igual a b e F no caso contrário.

2.5.1  Operadores aritméticos

Os operadores aritméticos, que podem ser usados com operandos de tipos aritméticos, são +, -, *, / e % (adição, subracção, multiplicação, divisão e resto da divisão inteira).  Existe ainda o operador - prefixo que, aplicado a um único operando (operador unário e não binário), tem como resultado o seu simétrico.

O significado do operador divisão depende do tipo dos operandos usados.  Por exemplo, o resultado de 10 / 20 é 0 (zero), e não 0,5.  I.e., se os operandos da divisão forem inteiros, então a divisão usada é a divisão inteira, sem que se usem "casas decimais".  Do mesmo modo, não faz sentido usar o operador % com operandos de vírgula flutuante, sendo esse operador reservado para operandos de tipos inteiros.

Os operadores de divisão e resto da divisão inteira estão especificados de tal forma que, se a e b forem valores inteiros, então a = (a / b) * b + a % b.

As operações aritméticas preservam os tipos dos operandos, i.e., a soma de dois float resulta num valor do tipo float, etc.  É possível, embora não recomendável, que os operandos sejam de tipos aritméticos diferentes, sendo feitas a conversão automática do operando com tipo menos abrangente para o tipo do operando mais abrangente.  Por exemplo, o código

const double pi = 3.1415927;
double x = 1 + pi;
leva o compilador a converter automaticamente o valor literal 1 do tipo int para o tipo double.  A estas conversões chama-se "conversões aritméticas usuais", e definem-se como se segue: Estas regras, apesar de se ter apresentado apenas uma versão resumida, são complexas e pouco intuitivas.  É preferível evitar que elas sejam necessárias!  Por exemplo, o código acima deveria ser reescrito como:
const double pi = 3.1415927;
double x = 1.0 + pi;
de modo a que o valor literal fosse do mesmo tipo que a constante pi.  Se não se tratar de um valor literal mas sim de uma variável, então é preferível converter explicitamente um ou ambos os operandos para compatibilizar os seus tipos:
const double pi = 3.1415927;
int i = 1;
double x = double(i) + pi;
Em qualquer dos casos é sempre boa ideia repensar o código para perceber se as conversões são mesmo necessárias, pois há algumas conversões que podem introduzir erros de aproximação indesejáveis ou, em alguns casos, desastrosos.

2.5.2  Operadores relacionais e de igualdade

Os operadores relacionais que podem ser usados em C++ são >, <, >= e <=, com os significados óbvios.  Para comparar a igualdade ou diferença de dois operandos usam-se os operadores de igualdade == (igual a) e != (diferente de).  Estes operadores têm como resultado não um valor aritmético mas sim um valor lógico, do tipo bool, sendo usadas comummente para controlar instruções de selecção e iterativas, que serão estudadas mais tarde.

2.5.3  Operadores lógicos

Os operadores lógicos (ou booleanos) que podem ser usados em C++ são ! (negação ou ¬), && (conjunção ou e) e || (disjunção ou ou).  Por exemplo:
a > 5           // verdadeira se a for maior que 5.
!(a > 5)        // verdadeira se a não for maior que 5.
a < 5 && b <= 7 // verdadeira se a for menor que 5 e b for menor ou igual a 7.
a < 5 || b <= 7 // verdadeira se a for menor que 5 ou b for menor ou igual a 7.
Estes operadores podem operar sobre operandos booleanos mas também sobre operandos aritméticos.  Neste último caso, o valor zero é interpretado como significando F e qualquer outro valor como significando V.

Os dois operadores binários && e || têm a particularidade importante de calcular os operandos (que podem ser sub-expressões) da esquerda para a direita, atalhando o cálculo logo que o resultado é conhecido: se o primeiro operando dum && é F, o resultado é F, se o primeiro operando dum || é V, o resultado é V, e em ambos os casos o segundo operando não chega a ser calculado.  Esta característica será de grande utilidade mais tarde.

2.5.4  Operadores bit-a-bit

Há alguns operadores do C++ que permitem fazer manipulações de muito baixo nível: ao nível do bit.  São as chamadas operações bit-a-bit que, dos tipos básicos do C++, apenas admitem operandos de tipos inteiros.  Muito embora estejam definidos para tipos com sinal, alguns têm resultados não especificados quando os operandos são negativos.  Assim, assume-se aqui que os operandos são de tipos sem sinal, ou pelo menos que são garantidamente positivos.  Os operadores são & (e bit-a-bit), | (ou bit-a-bit), ^ (oux bit-a-bit), ~ (negação bit-a-bit), << (deslocamento para a esquerda) e >> (deslocamento para a direita).  Estes operadores actuam sobre os bits individualmente.  Por exemplo:
123U & 0xFU == 11U == 0xBU
pois 123 = (00000000000000000000000001111011)2, (F)16 = (00000000000000000000000000001111)2
 e 11 = (00000000000000000000000000001011)2, que é o que se obtem quando se calcula o e lógico de cada par de bits em posições correspondentes nos dois operandos.

Os deslocamentos simplesmente deslocam o padrão de bits correspondente ao primeiro operando de tantas posições quanto o valor do segundo operando, inserindo zeros (0) à direita quando o deslocamento é para a esquerda e à esquerda quando o deslocamento é para a direita.  Por exemplo:

1U << 4U == 16U
20U >> 4U == 1U
É de notar que o deslocamento para a esquerda de n bits corresponde à multiplicação do inteiro por 2n e que o deslocamento de n bits para a direita corresponde à divisão inteira por 2n.

(Os exemplos assumem que os int têm 32 bits.)

2.5.5  Operadores de atribuição

A operação de atribuição, que é indicada pelo símbolo =, faz com que a variável que está à esquerda do operador tome o valor da expressão que se encontra à direita do operador.  Por exemplo:
a = 3 + 5; // a toma o valor 8.
Devem-se notar os significados distintos dos operadores = (atribuição) e == (comparação, igualdade).  É frequente o programador confundir estes dois operadores, levando a erros de programação difíceis de detectar.

A atribuição é uma operação como outra qualquer, mas com uma diferença: o seu cálculo afecta o valor da variável do seu lado esquerdo (por isso se diz que é uma operação "com efeitos laterais").  Assim, a operação tem um resultado: o resultado de uma atribuição é o valor que ficou guardado na variável do lado esquerdo da atribuição.  Estes facto, conjugado com a associatividade à direita deste tipo de operadores (ver mais abaixo), permite escrever

a = b = c = 1;
para atribuir 1 às três variáveis numa única instrução.  Note-se que o resultado de:
int i;
float f;
f = i = 1.9f;
é que i fica com o valor 1, pois a conversão de float para int elimina a parte decimal (não arredonda), e f fica com o valor 1.0f, pois é o resultado da conversão do valor 1 para float, sendo 1 o resultado da atribuição a i (o resultado duma atribuição é o valor que fica na variável a que se atribui o valor).

Existem vários outros operadores de atribuição em C++, que são formas abreviadas de escrever expressões comuns.  Assim, i += 4 tem (quase) o mesmo significado que i = i + 4.  Por exemplo:

2.5.6  Operadores de incrementação e decrementação

A expressões i += 1 e i -= 1, por serem tão frequentes, merecem também uma forma especial de abreviação: os operadores de incrementação e decrementação ++ e --.  Estes dois operadores têm duas versões: a versão prefixo e a versão sufixo.  Quando o objectivo é simplesmente incrementar ou decrementar uma variável, as duas versões têm o mesmo efeito:
i += 1; // ou
++i;    // ou
i++;
Porém, se o resultado da operação for usado numa expressão envolvente, as versões prefixo e sufixo têm resultados muito diferentes: o valor da expressão i++ é o valor de i antes de incrementado, ou seja, i é incrementado depois do seu valor ser extraído como resultado da operação, enquanto o valor da expressão ++i é o valor de i depois de incrementado, ou seja, i é incrementado antes do seu valor ser extraído como resultado da operação.  Assim:
int i = 0;
int j = i++;
cout << i << ' ' << j << endl;
escreve no ecrã os valores 1 e 0, enquanto
int i = 0;
int j = ++i;
cout << i << ' ' << j << endl;
escreve no ecrã os valores 1 e 1.

As mesmas observações aplicam-se às duas versões do operador --.

2.5.7  Precedência, associatividade e ordem de cálculo

Qual o resultado da expressão 4 * 3 + 2?  14 ou 20?  Qual o resultado da expressão 8 / 4 / 2?  1 ou 4?  Para que estas expressões não sejam ambíguas, o C++ estabelece um conjunto de regras de precedência e associatividade dos vários operadores possíveis.  A tabela abaixo lista os operadores do C++ por ordem decrescente de precedência.  Quanto à associatividade, apenas os operadores unários (com um único operando) e os operadores de atribuição se associam à direita: todos os outros associam-se à esquerda, como é habitual.
Precedência de operadores
Descrição Sintaxe
resolução de âmbito
resolução de âmbito
global
global
nome_de_classe :: membro
nome_de_espaço_nominativo :: membro
:: nome
:: nome_qualificado
selecção de membro
selecção de membro
indexação
invocação de função
construção de valor
pós incremento (sufixo)
pós decremento (sufixo)
identificação de tipo
identificação de tipo (durante a execução)
conversão verificada (durante a execução)
conversão verificada (durante a compilação)
conversão não verificada
conversão const
objecto . membro
ponteiro -> membro
ponteiro [ expressão_inteira ]
expressão ( lista_expressões )
tipo ( lista_expressões )
lvalor ++
lvalor --
typeid ( tipo )
typeid ( expressão )
dynamic_cast < tipo > ( expressão )
static_cast < tipo > ( expressão )
reinterpret_cast < tipo > ( expressão )
const_cast < tipo > ( expressão )
tamanho de objecto
tamanho de tipo
pré incremento (prefixo)
pré decremento (prefixo)
complemento (para um)
não
menos unário
mais unário
endereço de
desreferenciação
criar (alocar)
criar (alocar e inicializar)
criar (colocar)
criar (colocar e inicializar)
destruir (desalocar)
destruir matriz
coerção de tipo
sizeof expressão
sizeof ( tipo )
++ lvalor
-- lvalor
~ expressão
! expressão
- expressão
+ expressão
& lvalor
* expressão
new tipo
new tipo ( lista_expressões )
new ( lista_expressões ) tipo
new ( lista_expressões ) tipo (lista_expressões)
delete ponteiro
delete[] ponteiro
( tipo ) expressão
selecção de membro
selecção de membro
objecto .* ponteiro_para_membro
ponteiro ->* ponteiro_para_membro
multiplicação
divisão
módulo (resto)
expressão * expressão
expressão / expressão
expressão % expressão
soma
subtracção
expressão + expressão
expressão - expressão
desloca para a esquerda
desloca para a direita
expressão << expressão
expressão >> expressão
menor
menor ou igual
maior
maior ou igual
expressão < expressão
expressão <= expressão
expressão > expressão
expressão >= expressão
igual
diferente
expressão == expressão
expressão != expressão
E bit a bit expressão & expressão
OU exclusivo bit a bit expressão ^ expressão
OU (inclusivo) bit a bit expressão | expressão
E lógico (booleano) expressão && expressão
OU (inclusivo) lógico (booleano) expressão || expressão
expressão condicional expressão ? expressão : expressão
atribuição simples
multiplica e atribui
divide e atribui
módulo e atribui
soma e atribui
subtrai e atribui
desloca para a esquerda e atribui
desloca para a direita e atribui
E bit a bit e atribui
OU (inclusivo) bit a bit e atribui
OU exclusivo bit a bit e atribui
expressão = expressão
expressão *= expressão
expressão /= expressão
expressão %= expressão
expressão += expressão
expressão -= expressão
expressão <<= expressão
expressão >>= expressão
expressão &= expressão
expressão |= expressão
expressão ^= expressão
lança excepção throw expressão
vírgula (sequenciamento) expressão , expressão

Para alterar a precedência dos operadores numa expressão podem-se usar parênteses.  Por exemplo: x = (y + z) * w.  Podem-se usar os parênteses também para alterar o agrupamento de operações com a mesma precedência.  Por exemplo, x * y / z significa (x * y) / z, pois os dois operadores têm a mesma precedência e associam-se da esquerda para a direita.  Se o objectivo fosse calcular primeiro a divisão, então dever-se-ia escrever x * (y / z).  Note-se que o resultado, se os operandos forem inteiros, é quase sempre diferente: 4 * 5 / 6 resulta em 3 e 4 * (5 / 6) resulta em 0!  E o mesmo se passa com valores de vírgula flutuante, por pouco intuitivo que isso pareça.  Por exemplo, o seguinte troço de programa

// Para os resultados serem mostrados com 20 dígitos (usar #include <iomanip>):
cout << setprecision(20);
cout << 0.3f * 0.7f / 0.001f << ' ' << 0.3f * (0.7f / 0.001f)
     << endl;
escreve no ecrã
210 209.9999847412109375
onde se pode ver claramente os efeitos nefastos (mas inevitáveis) dos arredondamentos que afectam de forma diferente duas expressões que, do ponto de vista matemático, deveriam ter o mesmo valor.

Ordem de cálculo e efeitos laterais

O C++, ao classificar as várias formas de fazer atribuições como meros operadores, dificulta a distinção entre expressões bem comportadas e expressões mal comportadas ou com efeitos laterais.

Classificar-se-ão como expressões sem efeitos laterais as expressões cujo cálculo não afecta o valor de nenhuma variável, as expressões envolvendo atribuições sucessivas e que não ocorram como argumento de uma função nem como expressão de controlo duma instrução de selecção ou de um ciclo, sempre desde que não envolvam chamadas a funções ou procedimentos com efeitos laterais (matéria a abordar mais tarde).  Todas as outras terão efeitos laterais.  Assim:

x = y = z = 1;       // sem efeitos laterais: atribuições sucessivas.
x = y + (y = z = 1); // com efeitos laterais.
if(x == 0) ...       // sem efeitos laterais: não altera qualquer variável.
if(x++ == 0) ...     // com efeitos laterais.
while(cin >> x) ...  // com efeitos laterais.
Numa expressão, a ordem de cálculo dos operandos dum operador é indefinida (excepto no caso dos operadores booleanos && e ||, ver Secção 2.5.3).  Assim, na expressão:
y = sin(x) + cos(x) + sqrt(x)
não se garante que sin(x) seja calculada em primeiro lugar e sqrt(x) em último.  Se uma expressão não envolver operações com efeitos laterais, este facto não afecta o programador.  Mas quando a expressão tem efeitos laterais, e estes afectam variáveis usadas noutros locais da mesma expressão, esta pode deixar de ter resultados bem definidos, devido à indefinição quanto é ordem de cálculo.  Assim, depois das instruções
int i = 0;
int j = i + i++;
o valor de j pode ser 0 ou 1, consoante o operando i seja calculado antes ou depois do operando i++.

Este tipo de comportamento deve-se fundamentalmente a que, em C++, as atribuições são operações com um resultado, e não instruções especiais, como em Pascal *.  Este tipo de operações, no entanto, fazem parte do estilo usual de programação em C e C++, pelo que devem ser bem percebidas as suas consequências: uma expressão com efeitos laterais não pode ser interpretada como uma simples expressão matemática, pois há variáveis que mudam de valor durante o cálculo!  Expressões com efeitos laterais, são pois de evitar, salvo nas "expressões idiomáticas" da linguagem C++.

* No entanto, mesmo o Pascal não está livre deste tipo de problemas, pois funções ou procedimentos com argumentos passados por referência (var) podem alterar argumentos envolvidos na mesma expressão que envolve a sua chamada.

2.5.8  Exercícios

1.  Faça um programa que teste os operadores relacionais, de igualdade, lógicos e de atribuição abreviados referidos acima.  Verifique o resultado de cada operação.  O resultado da execução do seu programa deve ser aproximadamente:
Insira dois numeros inteiros: 3 5
Atribuí a m o valor 3
Atribuí a n o valor 5
O resultado de m-- é: 2
O resultado de n++ é: 6
O resultado de n += m é: 8
...
O resultado de n > m é: false
O resultado de n <= m é: true
Nota:  Para que os booleanos sejam escritos por extenso, faça: cout << boolalpha; no início do programa (experimente primeiro sem esta instrução e interprete o resultado).

2.6  Canais: leitura do teclado e escrita no ecrã

Para escrever no ecrã e ler valores do teclado usam-se os chamados canais (streams) de entrada e saída.  O canal de saída para o ecrã é designado por cout.  O canal de entrada de dados pelo teclado é cin.  Para efectuar uma operação de leitura de dados do teclado  usa-se o operador de extracção >>.  Para efectuar uma operação de escrita de dados no ecrã usa-se o operador de inserção <<.

Por exemplo,

int a = 10;
cout << "Vou escrever o valor de 'a' a seguir a esta frase: "
     << a;
e
int a;
cout << "Introduza um valor: ";
cin >> a;
Após uma operação de entrada de dados, a variável toma o valor que foi inserido no teclado pelo utilizador do programa, desde que esse valor possa ser tomado por esse tipo de variável.  Mais tarde se verá como verificar se o valor introduzido estava correcto, ou seja, como verificar se a operação de extracção teve sucesso.

Ao ser executada uma operação de leitura como acima, o computador interrompe a execução do programa até que seja introduzido algum valor no teclado.

Quando é feita a leitura de um caracter do teclado (usando extracção do canal cin) é lido apenas um caractere, mesmo que no teclado seja inserido um código (ou mais do que um caractere), i.e., o resultado das seguintes operações:

cout << "Insira um caractere: ";
char caractere;
cin >> caractere;
cout << "O caracter inserido é : " << caractere;
seria:
Insira um caractere: 48
O caracter inserido é: 4
caso o utilizador inserisse 48.  O dígito oito foi ignorado visto que se leu apenas um caractere, e não o seu código.

A utilização de canais de entrada e saída, associados ao teclado e ao ecrã, mas também a ficheiros arbitrários no disco, será vista mais tarde.

2.7  Exercícios

// Este programa lê dois números inteiros do teclado,
// multiplica-os, e escreve o resultado no ecrã.
#include <iostream>
using namespace std;

int main()
{
    cout << "Introduza dois números inteiros: " << endl;
    int primeiro_número, segundo_número;
    cin >> primeiro_número >> segundo_número;
    int resultado = primeiro_número * segundo_número;
    cout << "O resultado da multiplicação dos dois numeros é "
         << resultado << endl;
}

Nota:  A utilização de caracteres acentuados é válida, de acordo com a norma do C++.  Mas é usual os compiladores não os aceitarem senão nos comentários e entre aspas "".

1.  Passe para o editor o programa que se encontra acima e execute o programa.

2.  Desenvolva um pequeno programa que escreva no ecrã a frase "Olá mundo!".

3.a)  Escreva um programa que peça ao utilizador para inserir dois números inteiros e os some.  Depois de terminar o programa, o seu ecrã deve ter o seguinte aspecto:

Insira dois números: 12 234
A soma dos dois números inseridos é: 246
3.b)  Altere o programa de modo a que se possa inserir números decimais (com vírgula).

3.c)  Altere o seu programa para fazer a leitura de cinco números e somá-los.

3.d)  Se usou mais do que duas variáveis para fazer a alínea anterior, reescreva o seu programa usando apenas duas variáveis. Uma delas deve guardar o valor lido, enquanto a outra deve guardar o valor acumulado da soma dos números inseridos até ao momento.  Faça o traçado deste programa vizualizando a evolução de ambas as variáveis.

4.a)  Escreva um programa que, dado um caractere, escreva no ecrã o seu número de ordem no alfabeto (assuma código ISO-Latin-1).  Exemplo:

Por favor introduza um caractere: a
A ordem do caractere 'a' é: 1
Outro exemplo:
Por favor introduza um caractere: c
A ordem do caractere 'c' é: 3
4.b)  Escreva um programa que, dado um caractere, escreva no ecrã o caractere seguinte no alfabeto (assuma código ISO-Latin-1).

5.a)  Use o seu programa de soma (a versão feita para inteiros, na alínea 3.a)) para tentar somar os números 2147483647 e 2.  Faça um traçado do programa e tente perceber porque não funciona.

5.b)  Experimente "corrigir" o programa mencionado na alínea anterior usando variáveis do tipo float.  Funcionou?  Porquê?

6.  Corrija o seguinte programa:

#include <iostream>
using namespace std

int main(void)
{
    inteiro p1 p2 divisao,

    cout >> "Introduza dois numeros: << endl;
    cin >> primeiro_numero >> segundo_numero;
    primeiro_numero \ segundo_numero = divisao;
    cout << O Resultado e >> divisao ;
}

7.a)  Faça aparecer no ecrã a seguinte figura:
******
**  **
**  **
******
7.b)  Pense como faria para escrever uma caixa com medidas 50 x 70.  Volte a responder a este exercício após ter sido dado o conceito de "ciclo" (ou então aventure-se).

2.8  Referências

[1]  Bjarne Stroustrup, "The C++ Programming Language", terceira edição, Addison-Wesley, Reading, Massachusetts, 1998. *

[2]  Irv Englander, "The Architecture of Computer Hardware and Systems Software: An Information Technology Approach", John Wiley & Sons, Inc., Nova Iorque, 1996. *

[3] Samuel D. Conte and Carl de Boor, "Elementary Numerical Analysis", McGraw-Hill International Book Company, Auckland, 1983.

* Existe na biblioteca do ISCTE.