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.// 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.
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:
é 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.int a;
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:
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 = 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.
Define três variáveis todas do tipo int.int a, b, c;
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.
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.
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 a
1,7976931348623157 x 10+308 (e negativos) |
64 |
long double | número decimal | a mesma que double | 64 |
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.
|
|
|
|
|
|
|
|
Suponha-se agora que se soma 1 ao número representado por:
|
|
|
|
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,
|
|
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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.
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),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
---|---|---|---|
s = -1 se s0 = 1 |
|
|
|
s = -1 se s0 = 1 |
m = (m22...m0)2 x 2-23 |
|
s x m x 2-126 |
s = -1 se s0 = 1 |
= (1,m22...m0)2 |
e = (e7...e0)2 - 127 |
s x m x 2e |
s = -1 se s0 = 1 |
|
|
|
s = -1 se s0 = 1 |
|
|
|
É 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.
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:
aparece no ecrã:char caractere = 'a'; caractere = caractere + 1; cout << "O caracter seguinte é: " << caractere;
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 caracter seguinte é: b
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 *:
* 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.#include <iostream> using namespace std; int main(void) { for(int i = 0; i < 128; i++) { cout << "'" << char(i) << "' (" << i << ")" << endl; } }
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.
'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, 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 (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.
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; const char primeira_letra_do_alfabeto_latino = 'a';
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:; // expressão nula 2; 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:int i;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 (literal) 3".a = b + 3;
Outro exemplo:
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.int i, j; 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. 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.
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.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.
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 << 4 == 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 >> 3 == 1U
(Os exemplos assumem que os int têm 32 bits.)
Devem-se notar os significados distintos dos operadores = (atribuição) e == (comparação, igualdade).a = 3 + 5; // a toma o valor 8
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
para atribuir 1 às três variáveis numa única instrução.a = b = c = 1;
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:
i = i + n; i += n; i = i - n; i -= n; i = i * n; i *= n; i = i % n; i %= n; i = i / n; i /= n;
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:i += 1; // ou i++; // ou ++i;
escreve no ecrã os valores 1 e 0, enquantoint i = 0; int j = i++; cout << i << ' ' << j << endl;
escreve no ecrã os valores 1 e 1.int i = 0; 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 < 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
imprimecout << 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;
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.210 209.9999847412109375
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: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.
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çõesy = sin(x) + cos(x) + sqrt(x)
o valor de j pode ser 0 ou 1, consoante o operando i seja calculado antes ou depois do operando i++.int i = 0; int j = i + 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.
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).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
A entrada de dados do teclado tem o seguinte aspecto:int a = 10; cout << "Vou escrever um número a seguir a esta frase: " << 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.cin >> a;
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:
seria:cout << "Insira um caractere: "; char caractere; cin >> caractere; cout << "O caracter inserido é : " << caractere;
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.Insira um caractere: 48 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ão discutidos em pormenor mais tarde.
Notas:// 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; }
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:
3.b) Altere o seu programa de modo a que possa inserir números decimais (com vírgula).Insira dois números : 12 234 A soma dos dois números inseridos é: 256
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:
Outro exemplo :Por favor introduza um caracter: a A ordem do caracter 'a' é: 1
4.b) Escreva um programa que, dado um caracter, escreva no ecrã o caracter seguinte no alfabeto (assuma código ASCII).Por favor introduza um caracter: c A ordem do caracter '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:
7.a) Faça aparecer no ecrã a seguinte figura:#include <iostream>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 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.