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 na forma duma linguagem de programação.  Um programa em C++ consiste de um conjunto de instruções (sem qualquer tipo de ambiguidade) que são executadas (cegamente) duma forma sequencial.  As instruções alteram o valor de variáveis, isto é, alteram o estado da memória associada ao programa, modificam o fluxo de controlo, ou definem (ou declaram) entidades (variáveis, constantes, funções, etc.).  Neste capítulo apresentar-se-á um conjunto de instruções básicas existentes em todos os programas e as formas de definir variáveis e de utilizar ou modificar 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 "apenas" para documentar os programas):
// 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 nosso programa vai
// começar 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 mais tarde):
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 dos canais de entrada e saída de dados a partir do teclado e para o ecrã.  A linha seguinte, using namespace std;,  é aquilo a que se chama 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).  Quanto a main, não passa duma função que tem a particularidade de ser a primeira a ser invocada no programa.  Estes assuntos serão vistos em pormenor mais tarde.

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 fazem algo de útil.  As linguagens de alto nível, como o C++, "escondem" a memória por trás do conceito de variável.  Assim, variáveis são na realidade pedaços de memória a que atribuímos um nome, e cujo conteúdo é interpretado de acordo com o seu tipo.

Os tipos não passam duma abstração.  Tudo o que representamos na memória do computador corresponde a padrões de bits, os famosos "zeros e uns".  O tipo duma variável indica simplesmente como um dado padrão de bits deve ser interpretado.

Cada variável tem duas características: um nome e um tipo.  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).  Uma instrução onde se pede para criar uma variável com um dado nome e dum determinado tipo denomina-se definição.  A instrução:

int a;
é uma definição de uma variável chamada a que pode guardar valores do tipo int (inteiros).  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.

Uma forma intuitiva de var 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.  Outra restriçã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.

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).  Essa posição de memória contém um padrão de bits 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 pode (e deve-se sempre que possível e razoável) atribuir-se-lhe um valor inicial.  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 = 256;  // definição de uma variável 'a' do tipo int com valor
              // inicial 256.
int a(256);   // idem.
int a;        // estas duas instruções sucessivas têm o mesmo efeito das
a = 256;      // anteriores.
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, b, c;
Define três variáveis todas do tipo int.

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, e também pelo caractere '_' (underscore), que se usa normalmente para tornar mais legível o nome da variável.  O nome de uma variável não pode ter espaços nem pode começar por um dígito.  O compilador, antes de fazer uma análise sintáctica do programa, faz uma análise lexical.  Esta análise é "gulosa": os símbolos (palavras, sinais de pontuação, etc.) detectados 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.

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++.  No entanto, em aulas posteriores estudar-se-ão formas de estender a linguagem com outros tipos, mais potentes: a isso de chama programação baseada em objectos (programação orientada para objectos é ainda outro conceito).

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 à 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.
 

Tipos básicos elementares
Tipo Descrição Gama Bits (em Visual C++)
bool valor booleano true e false (0 e 1) 8
int número inteiro -231 a 231-1 
(-2147483648 a 2147483647)
32
float número decimal 
(com vírgula)
1,17549435 x 10-38
3,40282347 x 10+38 (e negativos)
32
char caractere 
(código ASCII)
Qualquer caractere. Exemplos: 
'a', 'A', '1', '!', '*', etc.
8
Os caracteres (char) são interpretados como inteiros em C++.  Assim, 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.
 
Outros tipos básicos
Tipo Descrição Gama Bits (em Visual C++)
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 x 10-308
1,7976931348623157 x 10+308
(e negativos)
64
long double número decimal a mesma que double 64
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 devemos sempre estar cientes das limitações de qualquer tipo.  Por vezes, no entanto, a representação é muito relevante, nomeadamente quando programando 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á, a seu tempo, tratada pormenorizadamente 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 (no Visual C++ têm 32 bits).  Podemos representar esquematicamente um dado unsigned int como:
 
b3
b2
b1
b0
em que 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 num destes unsigned int.  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 (de facto, 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 se soma 1 ao número representado por:
 
1
1
1
1
O resultado é (1111 + 1)2 = (10000)2.  Mas este valor não é representável num unsigned int de quatro bits!  O que acontece tipicamente num caso destes é que são guardados apenas os 4 bits menos significativos do resultado, pelo que, no computador hipotético, (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 Visual C++.

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 a 0, o valor representado é x.  Pelo que se disse anteriormente, somar 1 a x corresponde a rodar o ponteiro das 15 para as 0 horas (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 -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 ficarão muito claras.

Como saber, olhando para o 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.  Nesse 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 = (1000)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, como nos inteiros, mas quatro posições para a esquerda.  O menor valor (em módulo) 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 (em módulo) representável é 1/216 = (aproximadamente) 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 flutuante.  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 de representados em 32 bits (i.e., de precisão simples),

  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 x 2-23
0
valores não normalizados*
s x m x 2-126
s = 1 se s0 = 0 
s = -1 se s0 = 1
m = 1 + (m22...m0)2 x 2-23
= (1,m22...m0)2
1 a 254 
e = (e7...e0)2 - 127
valores normalizados*
s x m x 2e
s = 1 se s0 = 0 
s = -1 se s0 = 1
0
255
s x 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 normalizado (em módulo) representável é 1 x 2-126 = (aproximadamente) 1,17549435 x 10-38 e o maior é (224 - 1) x 2-23 x 2127 = (aproximadamente) 3,40282347 x 10+38 (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.

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 em 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, é sempre preferível usá-los a recorrer aos tipos de vírgula flutuante, que, apesar da sua aparência inocente, reservam muitas surpresas: 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).

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 de um 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.

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 comum é a tabela dos códigos ASCII.  A tabela de codificação usada no Windows NT usa uma extensão do ASCII que permite usar caracteres acentuados.  Existem muitas dessas extensões, que podem ser usadas de acordo com o país em que se está.  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.  Assim, para referir o código do caractere 'b' usa-se 'b', que tem o mesmo significado que 98 (em ASCII).  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.  É possível tratar um char como um pequeno número inteiro.  Por exemplo, se se executar o conjunto de instruções seguinte:

char caractere = 'a';
caractere = caractere + 1;
cout << "O caracter seguinte é: " << caractere;
aparece no ecrã:
O caracter seguinte é: b
O que sucedeu foi que se adicionou 1 ao código do caracter 'a', de modo que a variável caractere passou a conter o código do caractere 'b'.  Este pedaço de código só é garantidamente válido se se souber que, na tabela que estamos a usar (ASCII), o alfabeto possui códigos sequenciais.

O programa seguinte imprime no ecrã todos os caracteres do código ASCII (que só especifica os caracteres correspondentes aos códigos de 0 a 127, isto é, todos os valores positivos dos char em Windows NT *:

#include <iostream>
using namespace std;

int main(void)
{
    for(int i = 0; i < 128; i++) {
        cout << "'" << char(i) << "' (" << i << ")" << endl;
    }
}
* 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.

2.2.4  Inicialização de variáveis

As posições de memória contêm sempre um padrão de bits: não há posições "vazias".  Assim, as variáveis têm sempre um valor qualquer.  Depois duma definição, e se não for feita uma inicialização explícita conforme sugerido mais atrás, a variável definida contém (em geral) um valor arbitrário.  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 classes de variáveis (ver secção sobre Permanência de variáveis) que são inicializadas implicitamente com zero.  Evite fazer uso dessa característica do C++: inicialize-as sempre explicitamente.

2.3  Valores literais

Valores literais permitem-nos indicar valores específicos 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 (e não float).
100.0F  // do tipo float (e não double).
100.0L  // do tipo long double.
1.1e230 // do tipo double, valor 1,1 x 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, 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.  Isso é feito usando a palavra-chave const.  As constantes, justamente por o serem, têm obrigatoriamente de ser inicializadas no acto da definição.  Por exemplo:
const int primeiro_primo = 2;
const char primeira_letra_do_alfabeto_latino = 'a';

2.5  Expressões e operadores

Em C++, o tipo de instrução mais simples 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
2;
1 + 2 * 3;
Para que os programas tenham algum interesse, é necessário que "algo aconteça", 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 alterando os valores das variáveis (e não só, como se verá mais tarde) 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 interessantes:
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 (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) 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 aritméticos, são +, -, *, / e % (adição, subracção, multiplicação, divisão e resto da divisão inteira [divisão de inteiros]).  Existe ainda o operador - prefixo que, aplicado a um único operando (operador unário e não binário), calcula 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.  Do mesmo modo, não faz sentido usar o operador % com operandos de vírgula flutuante, sendo esse operador reservado para operandos 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 é igual 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.  Mais tarde se verá que é possível que os operandos sejam de tipos aritméticos diferentes, sendo feitas conversões automáticas.

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.

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 maior que 5.
a < 5 && b <= 7 // verdadeira se a menor que 5 e b menor ou igual a 7.
a < 5 || b <= 7 // verdadeira se a menor que 5 ou b 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 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.  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 << 4 == 16U
20U >> 3 == 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 sinal =, faz com que a variável que está à esquerda do sinal tome o valor da expressão que se encontra à direita do sinal.  Por exemplo:
a = 3 + 5; // a toma o valor 8
Devem-se notar os significados distintos dos operadores = (atribuição) e == (comparação, igualdade).

A atribuição é uma operação, pelo que deve ter um valor: o valor 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, permite escrever

a = b = c = 1;
para atribuir 1 às três variáveis numa única instrução.

Existem muitos outros operadores de atribuição em C++, que são formas abreviadas de escrever expressões muito comuns.  Assim, i += 1 tem (quase) o mesmo significado que i = i + 1.  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 valor da operação for usado para algo mais, 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 expressã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 expressã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 < type > ( expressão )
static_cast < type > ( expressão )
reinterpret_cast < type > ( expressão )
const_cast < type > ( 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 seja.  Por exemplo, o seguinte troço de programa

cout << setprecision(20); // para os resultados serem mostrados com 20 dígitos
cout << 0.3f * 0.7f / 0.001f << ' ' << 0.3f * (0.7f / 0.001f) << endl;
imprime
210 209.9999847412109375
onde se podem 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 atribuição 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 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 das sub-expressões é 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 utilizador.  Mas quando a expressão tem efeitos laterais, e estes afectam variáveis usadas noutros locais da 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 valor, 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!  Expressões com efeitos laterais, são pois de evitar, salvo em "expressões idiomáticas" da linguagem C++.

* No entanto, mesmo o Pascal não está livre deste tipo de problemas, pois funções 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 usa-se aquilo a que se chama canais de entrada e saída (streams).  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 entrada de dados usa-se o operador >>.  Uma instrução de escrita de dados no ecrã tem o seguinte aspecto:
int a = 10;
cout << "Vou escrever um número a seguir a esta frase: " << a;
A entrada de dados do teclado tem o seguinte aspecto:
cin >> a;
Após uma operação de entrada de dados, a variável toma o valor que foi por nós inserido no teclado, desde que esse valor possa ser tomado por essa variável.  Mais tarde veremos como verificar se o valor introduzido estava correcto.  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 (através de cin) o que é 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 número 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ão discutidos em pormenor 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; 
}
Notas:
  1. A utilização de caracteres acentuados é válida, de acordo com a norma do C++.  Mas o Visual C++ não os aceita senão em comentários e entre "".
  2. Podem haver algumas surpresas com os caracteres acentuados colocados entre "" quando eles são escritos no ecrã (em Windows NT + Visual C++).
  3. Logo, a sua utilização pode não ser recomendável.
1.  Passe para o editor o programa que se encontra acima e corra 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 é: 256
3.b)  Altere o seu programa de modo a que 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, re-escreva 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 caracter, escreva no ecrã o seu número de ordem no alfabeto (assuma código ASCII).  Exemplo:

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

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 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.