Guião da 2ª Aula Teórica

Sumário

  1. Conceito de memória.
  2. Variáveis como forma estruturada de usar a memória em C++.
  3. Tipos básicos em C++: bool, int (e variantes), float (e variantes) e char.
  4. Noções sobre a representação física dos tipos.
  5. Noções de representação de inteiros em complemento para dois.
  6. Noções de representação de valores em vírgula flutuante: mantissa e expoente.
  7. Noções sobre códigos de caracteres.
  8. Interpretação de valores do tipo char como inteiros.
  9. Valores literais.
  10. Computadores como máquinas finitas: limitações dos tipos e sua importância.
  11. Expressões e operações: operações aritméticas, relacionais e de igualdade.
  12. Precedência e associatividade.
  13. Importância da operação de atribuição: alteração do estado da memória.

Alertar para importância de ler as folhas teóricas: as aulas não cobrem toda a matéria!

Na nossa analogia da culinária, em que uma receita correspondia a um algoritmo, a que é que corresponde o cozinheiro?

O cozinheiro executa instruções da receita usando os recipientes, o frigorífico e a despensa para preparar os cozinhados.  O processador executa instruções de um programa usando as variáveis (memória rápida) e ficheiros no disco rígido (memória lenta) para produzir os seus resultados.

As variáveis são portanto a nossa maneira de usar a memória rápida do computador em C++.  As variáveis são nomes que nós atribuímos a pedaços de memória.

Em culinária cada recipiente tem um tipo, que enquadra e limita as suas utilizações.  Por exemplo, um pimenteiro é usado para guardar grãos de pimenta.

Também em C++.

Por exemplo:

bool encontrado = false;

Esta instrução define uma nova variável com:
  1. Nome: encontrado.
  2. Tipo: bool, ou seja Booleano (falar do Sr. Boole), só pode tomar os valores falso e verdadeiro.
  3. Valor inicial: falso.
George Boole (1815-1864), matemático inglês.  Publicou "An investigation into the Laws  of Thought, on Which are founded the Mathematical Theories of Logic and Probabilities" em 1854.

Esta variável, quando o programa é compilado e executado, é colocada algures na memória.

No nosso computador de papel e lápis, pensem na memória como um bloco de notas em que cada folha pode ser destinada a uma variável.

Desenhar bloco de notas em que a primeira página se chama encontrado e contém o valor falso.  Cada página é desenhada em UML.

Outros exemplos:

Chamar "decimal" aos float.

int número_de_alunos = 30;
float f = 0.0f;
char c = 'A';

Explicar cada um dos exemplos.  Caracteres não são só letras!  Explicar que 30, 0.0f, e 'A' são valores literais.  Isto é, constantes cujos valores são indicados explicitamente.

O que é a memória do computador?  Uma sequência de bits.  Quantos?  Na mercúrio são 768 Mbytes, ou seja, 805306368 octetos, que são 6442450944 bits!  Exacto, seis mil milhões, quatrocentos e quarenta e dois milhões, quatrocentos e cinquenta mil, novecentos e quarenta e quatro!  Aproximadamente um bit por cada habitante do planeta.  Ou ainda 150 000 páginas densas em 300 livros de quinhentas páginas.

Como são representadas estas variáveis na memória do computador?

Tipicamente cada caractere (variável do tipo char) é representado por oito bits (i.e., dígitos binários, com valor 0 ou 1), formando um padrão.  Quantos diferentes padrões é possível formar com 8 bits?

A resposta é 28, ou seja, 256.  Se se fizer corresponder cada padrão a um caractere (um símbolo gráfico: letra, dígito decimal, sinal de pontuação, etc.), podem-se representar 256 diferentes caracteres.

É suficiente para português, sê-lo-á para o chinês?

A correspondência usada entre os padrões de bits e os caracteres é chamada um código.  O código mais frequente nesta ponta da Europa é o código ISO-Latin-9 (ISO-8859-15), que veio substituir o ISO-Latin-1 (ISO-8859-15) aquando da introdução do euro, e que é uma extensão do código ASCII (American Standard Code for Information Interchange).

Nesse código ISO-Latin-9:

'A' é representado pelo padrão: 01000001
'a' é representado pelo padrão: 01100001
'0' é representado pelo padrão: 00110000

Num outro código, praticamente caído em desuso, como o EBCDIC (Extended Binary Coded Decimal Interchange Code):

'A' é representado pelo padrão: 11000001
'a' é representado pelo padrão: 10000001
'0' é representado pelo padrão: 11110000

Estes padrões costumam ser interpretados como números inteiros em base binária.  

Fazer brevíssima revisão, pois em Arquitectura de Computadores já lhes devem ter ensinado esta matéria.

Assim, em ISO-Latin-1:

'A' é representado pelo padrão: (01000001)2 = (65)10
'a' é representado pelo padrão: (01100001)2 = (97)10
'0' é representado pelo padrão: (00110000)2 = (48)10

Note-se que o caractere '0' é representado pelo padrão 00110000, ou seja, 48 na base 10!

Será que temos de nos preocupar com a representação em bits ou com o código em decimal dos caracteres?  Em princípio não.  Se quisermos guardar um 'A' numa variável fazemos:

char caractere = 'A';

e não precisamos de saber o código, ou seja, não é necessário fazer:

char caractere = 65;

I.e., os valores literais (constantes com valor explícito) do tipo char escrevem-se envolvendo o caracter em causa entre plicas.

Porquê têm os valores literais de caracteres de ser escritos entre plicas?  Para distinguir os caracteres das variáveis normais:

char c = ...;
char a =
...;

c = a;
c = 'a';

Qual a diferença?

Discutir.

Compare-se com a frase em português:

O Joaquim tem cinco vogais.

Está correcta?  E se fosse

"O Joaquim" tem cinco vogais.

?

E os inteiros?  Como se representam?  Os int têm tipicamente 32 bits.  Para simplificar vamos imaginar que tinham somente 4 bits.  Quantos valores diferentes se podem representar com quatro bits? 16!

Se os int não pudessem representar valores negativos, que valores representariam?

Discutir numeração binária de novo se necessário.  Explicar que são valores de 0 a 24-1.  Generalizar.

Que acontece se se somar 1 a 15?

 1111
 0001 +
10000

mas o bit mais significativo não cabe nos nossos inteiros de quatro bits!  Logo, o resultado é.... 0 (zero)!

Que acontece se se somar 2 a 14?

 1110
 0010 +
10000

mas o bit mais significativo não cabe nos nossos inteiros de quatro bits!  Logo, o resultado é.... 0 (zero) de novo!

É mais claro se desenharmos os padrões de bits num relógio.

Colocar 0000 em cima.  Primeiro desenhar sem os valores em decimal.  Depois colocar os valores em decimal.

E se se pretendesse representar também valores negativos?  Qual seria o padrão de bits candidato a representar -1?

Apagar os valores decimais maiores.

Discutir com eles.  A resposta é 1111.

E qual o candidato a representar -2?  Não é o 1110?

Onde se deve parar?

Discutir com eles.  Possibilidades seriam parar em -7 ou -8.  Claro que não se pode parar em zero, porque não sobrariam positivos.

As razões porque se pára em -8 são simples:

  1. Temos aproximadamente o mesmo número de positivos e negativos.
  2. Os negativos podem ser identificados como tal olhando apenas para um bit!  Qual?
Qual é então a gama de valores representáveis?

Discutir e concluir que é -8 a 7.

Que fazer se se quiser saber a representação de -5 sem olhar para o relógio?  Simples, basta subtrair 5 de 16:
 

10000
 0101 -
 1011
E se os nossos inteiros tivessem n bits, qual seria a gama representável?

Discutir porque é que a resposta é: -2n-1 a 2n-1-1.

A este tipo de representação dos inteiros sem sinal chama-se complemento para 2 e será aprofundada nas aulas de Arquitectura de Computadores.

Quando se define uma variável com tipo int, está-se a definir uma variável com 32 bits e representação em complemento para 2, e que portanto que pode tomar valores entre...

-2147483648 e 2147483647

Os computadores são finitos!  As variáveis não guardam valores arbitrários!

Aqui falar no bug do ano 2000.

E o bug do ano 2038?  Conhecem-no?

O tempo em C++ é contado em segundos a partir de 1970 e guardado em variáveis do tipo int.  Como os int têm 32 bits, a gama de valores é de -2147483648 a 2147483647.  Isso são aproximadamente 68 anos.  Logo, em 2038 passa-se para...  1902...

Os caracteres também são interpretados como inteiros.  Que valor inteiro tem o caractere 'A'?  Como se usa o código ISO-Latin-9, o padrão de 8 bits correspondente ao caractere 'A' é (01000001)2 = (65)10.  Que aparecerá então no ecrã dadas as instruções

char caractere = 'A';
int código = caractere;
cout << código << endl;

?

Aparece 65!  E se fosse

char caractere = 'A';
int código = caractere + 1;
cout << código << endl;

?

Apareceria 66!  E se fosse

char caractere = 'A';
cout << caractere << endl;
++caractere;
cout << caractere << endl;

?

Apareceria

A
B

Mas só aparece 'B' porque se está a usar um código em que os códigos correspondentes às letras do alfabeto são sucessivos!  Em EBCDIC 'I' é 137 mas 'J' é 145!

E os float?  Como são representados?  Os float representam valores decimais na forma de padrões de 32 bits.

Fazer boneco!  Incluir s, m e e.  Ver figuras das folhas teóricas.

Dos 32 bits:

O valor representado num float é s m 2e.  Ou seja,  o valor maior representável é aproximadamente 2x2127 ou seja 3,4028x1038.  E o mais pequeno positivo é aproximadamente 1x2-126 ou seja 1,1755x10-38.

Explicar bem porque é que (1,11111111111...)2 é aproximadamente 2.  Aliás, neste caso é 2 - 2-23.

Que tipos básicos existem?

Valores literais:

true - bool
10
- int
10L
- long int
10U
- unsigned int
'A'
- char
'.'
- char
10.0
- double
10.0f
- float
10.0L
- long double
3.3e3
= 3300.0 - double

Mudando de assunto, agora que se viram as variáveis, seus tipos e representação, o que é um programa em C++?  É um conjunto de instruções.  A instrução mais usual é:

expressão;

Em que expressão é uma expressão envolvendo os operadores do C++.  O operador mais importante é o de atribuição.  Este operador tem a forma

variável = expressão;

e tem como resultado atribuir o valor da expressão à variável do lado esquerdo.  É uma operação que afecta o estado do programa, pois altera o valor de uma variável.

Deixar claro que uma instrução consistindo apenas numa expressão que não afecta o estado do programa não é muito interessante: daí o interesse da atribuição.  Referir que o estado do programa é o valor das suas variáveis.

Outros operadores interessantes são os aritméticos: +, -, *, / e %.  Destes a divisão / e o resto da divisão % merecem menção especial.

A divisão / é a divisão inteira se os operandos forem inteiros.  

Que fica em x depois de:

int x = 1 / 2;

?

Discutir!  É 0 (zero).

O resto da divisão inteira só se pode aplicar a operandos inteiros:

int x = 1.0 % 2.0;

dá erro de compilação!  O tradutor para linguagem máquina (compilador) não aceita a expressão.

Que fica em x depois de:

int x = 15 % 6;

?

Fica 3.

O tipo do resultado é o tipo dos operandos.  Se os operandos forem de tipos diferentes, o operando de um tipo "menos potente" é convertido para o tipo "mais potente" antes do cálculo.  Por exemplo:

cout << 'a' + 1 << endl;

o que é escrito?

Discutir.  Não é 'b', é 98!

Existem também os operadores relacionais >, >=, <, <= e de igualdade e diferença == e !=.  O resultado é um bool com valor verdadeiro (literal  true) ou falso (literal false).

Os operadores respeitam regras de precedência e associatividade.  Por exemplo:

cout << 1 + 5 * 3 << endl;

escreve 18 ou 16?

É 16, porque * tem precedência face a +.

Por exemplo:

cout << 16 / 2 / 2 << endl;

escreve 16 ou 4?

É 4, porque / associa-se à esquerda.

Quase todos os operadores em C++ se associam à esquerda.  A atribuição é uma excepção:

int i, j;
i = j = 0;

tem o resultado esperado.

Explicar que o valor da operação de atribuição é o valor que fica na variável atribuída.

Exemplo:

int x;
double f;
f = x = 1.3;

Que fica em f?

Discutir.  double para int é possível, mas perde parte decimal.  É preferível explicitar a conversão:

int x;
double f;
f = x = int(1.3);

E melhor ainda é fazer apenas uma atribuição por instrução.

Explicar que a atribuição é uma operação com efeitos laterais, ao contrário da adição, por exemplo.

É comum encontrar-se expressões do tipo:

x = x + 10

O C++ permite abreviar estas expressões para:

x += 10

+=, -=, *=, /=, %= são todos operadores especiais de atribuição.

Finalmente, também são frequentes expressões do tipo:

x = x + 1
x += 1

O C++ permite abreviar estas expressões para:

++x

ou
x++
Recomendar vivamente a primeira!

E de igual modo para a subtracção.  Chamam-se operadores de incrementação (++) e decrementação (--) prefixos ou sufixos.

Na versão prefixo o valor da operação é o valor da variável depois de incrementada.  Na versão sufixo o valor da operação é o valor da variável antes de incrementada.  Compare-se:

int i = 0;
int j = ++i;

com

int i = 0;
int j = i++;

No primeiro caso j termina com 1, no segundo com 0!  Em ambos os casos i fica com 1.  Perceberam?

Discutir.