// As duas linhas seguintes são necessárias para permitir aA 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).
// 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 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.
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.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(10); // forma alternativa.
define três variáveis todas do tipo int.int a = 0, b = 1, c = 2;
* Na realidade as duas formas não são rigorosamente equivalentes (ver nas Curiosidades).
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.
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.
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 a
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.
|
|
|
|
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:
|
|
|
|
é o padrão de bits correspondente ao valor (1001)2, ou seja 9 em decimal.
Suponha-se agora que a variável contém
|
|
|
|
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,
|
|
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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.
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++),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Os valores representados são dados pela tabela que se segue:
|
|
|
|
---|---|---|---|
s = -1 se s0 = 1 |
|
|
|
s = -1 se s0 = 1 |
m = (m22...m0)2 × 2-23 |
|
s × m × 2-126 |
s = -1 se s0 = 1 |
= (1,m22...m0)2 |
e = (e7...e0)2 - 127 |
s × m × 2e |
s = -1 se s0 = 1 |
|
|
|
s = -1 se s0 = 1 |
|
|
|
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.
int i = int(true);inicializa a variável i com o valor inteiro 1.
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';aparece no ecrã:
++caractere; // o mesmo que caractere = caractere + 1;
cout << "O caracter seguinte é: " << caractere << endl;
O caracter seguinte é: jO 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) *:
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.#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.
* 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.
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.
'a' // do tipo char, representa o código do caractere 'a'.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:
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.
0x1U // o mesmo que 1U.(Os exemplos assumem que os int têm 32 bits.)
0x10FU // o mesmo que 271U, ou seja (00000000000000000000000100001111)2.
077U // o mesmo que 63U, ou seja (00000000000000000000000000111111)2.
const int primeiro_primo = 2;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
const char primeira_letra_do_alfabeto_latino = 'a';
double raio = 3.14;é preferível
cout << "O perímetro é " << 2.0 * 3.14 * raio << endl;
cout << "A área é " << 3.14 * raio * raio << endl;
const double pi = 3.14;Há várias razões para ser preferível a utilização de constantes no código acima:
double raio = 3.14;
cout << "O perímetro é " << 2 * pi * raio << endl;
cout << "A área é " << pi * raio * raio << endl;
; // expressão nula (instrução nula).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:
2;
1 + 2 * 3;
int i;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:
i = 2;
i = 1 + 2 * 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".a = b + 3;
Outro exemplo:
int 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.
bool i_é_igual_a_j;
// ...
i_é_igual_a_j = i == j;
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;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:
double x = 1 + pi;
const double pi = 3.1415927;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:
double x = 1.0 + pi;
const double pi = 3.1415927;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.
int i = 1;
double x = double(i) + pi;
a > 5 // verdadeira se a for maior que 5.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.
!(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.
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.
123U & 0xFU == 11U == 0xBUpois 123 = (00000000000000000000000001111011)2, (F)16 = (00000000000000000000000000001111)2
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É 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.
20U >> 4U == 1U
(Os exemplos assumem que os int têm 32 bits.)
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
para atribuir 1 às três variáveis numa única instrução. Note-se que o resultado de:a = b = c = 1;
int i;é 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).
float f;
f = i = 1.9f;
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:
i += 1; // ouPoré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:
++i; // ou
i++;
int i = 0;escreve no ecrã os valores 1 e 0, enquanto
int j = i++;
cout << i << ' ' << j << endl;
int i = 0;escreve no ecrã os valores 1 e 1.
int j = ++i;
cout << i << ' ' << j << endl;
As mesmas observações aplicam-se às duas versões do operador --.
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>):escreve no ecrã
cout << setprecision(20);
cout << 0.3f * 0.7f / 0.001f << ' ' << 0.3f * (0.7f / 0.001f)
<< endl;
210 209.9999847412109375onde 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.
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.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:
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.
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;o valor de j pode ser 0 ou 1, consoante o operando i seja calculado antes ou depois do operando i++.
int j = i + 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.
Insira dois numeros inteiros: 3 5Nota: 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).
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
Por exemplo,
int a = 10;e
cout << "Vou escrever o valor de 'a' a seguir a esta frase: "
<< a;
int 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.
cout << "Introduza um valor: ";
cin >> a;
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: ";seria:
char caractere;
cin >> caractere;
cout << "O caracter inserido é : " << caractere;
Insira um caractere: 48caso o utilizador inserisse 48. O dígito oito foi ignorado visto que se leu apenas um caractere, e não o seu código.
O caracter inserido é: 4
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.
// Este programa lê dois números inteiros do teclado,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 "".
// 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;
}
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 2343.b) Altere o programa de modo a que se possa inserir números decimais (com vírgula).
A soma dos dois números inseridos é: 246
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: aOutro exemplo:
A ordem do caractere 'a' é: 1
Por favor introduza um caractere: c4.b) Escreva um programa que, dado um caractere, escreva no ecrã o caractere seguinte no alfabeto (assuma código ISO-Latin-1).
A ordem do caractere 'c' é: 3
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>7.a) Faça aparecer no ecrã a seguinte figura:
using namespace stdint 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.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] 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.