De Objective Caml para C e C++/Instruções

Instruções de expressão editar

As instruções de expressão compõem a forma mais simples de construir uma instrução, pois permite instruir o compilador que uma dada expressão deve ser avaliada nesse ponto. A sintaxe da instrução de expressão é:

expressao;

Ou seja, é apenas uma expressão seguida de um ponto e vírgula. A seguir vamos então ver quais são as expressões que são mais frequentemente encontradas em instrução expressão.

Além das expressões sobre os tipos básicos já apresentadas, existem uma série de expressões diferentes providas que permitem compor os blocos básicos de algoritmos seqüênciais. São elas:

  • Atribuição;
  • Chamada de função;
  • Pós-incremento;
  • Pré-incremento;
  • Atribuição composta;
  • Composição sequencial.

Atribuição editar

A sintaxe da atribuição em C e C++ é a seguinte:

alvo = expressao

A semântica dessa construção é a seguinte: a expressão fonte expressao é avaliada, e o valor resultante dessa avaliação é memorizado por alvo. Qual a natureza do alvo? Os conceitores da linguagem chamam alvo de lvalue. Esse conceito é similar ao de uma referência em Objective Caml: trata-se de uma entidade que pode guardar um valor. Por enquanto, a única classe de entidades que vimos que podem ser lvalues são as variáveis.

Segue um pequeno exemplo:

int i, j;
i = 0;
j = i + 2;

Nesse trecho de código, a primeira linha declara i e j como sendo variáveis do tipo inteiro, cujo valor inicial é indefinido. Na segunda linha, é realizada uma atribuição. O alvo é a variável i e a expressão é o valor inteiro 0. Após a execução dessa instrução, i passa a guardar o valor 0. Na terceira linha, há uma segunda atribuição. O alvo é a variável j e a expressão fonte é i + 2, cujo valor é  . Após essa instrução, o valor memorizado por j passa a ser 2 e o valor guardado por i continua sendo 0.

O operador de atribuição tem um valor de retorno, que é o próprio valor atribuído. Ele também tem a propriedade de ser associativo a direita. Essas duas propriedades fazem com que seja possível realizar diversas atribuições encadeadas em uma única instrução:

int alt, larg;
alt = larg = 5;

alt e larg são ambas variáveis do tipo inteiro com valor inicial indefinido. A segunda linha é um encadeamento de atribuições. Como o operador de atribuição é associativo a direita, a expressão deve ser lida assim: alt = (larg = 5). A atribuição à variável alt é realizada, fazendo a avaliação da expressão larg = 5 que é uma outra atribuição. Essa segunda atribuição é então avaliada fazendo a avaliação da expressão fonte 5 cujo resultado (o número  ) é atribuído à variável larg e é retornado como valor da expressão larg = 5. Esse valor é atribuído à variável alt e é o resultado da expressão alt = larg = 5. Ao final então da avaliação dessa expressão, ambas variáveis alt e larg guardam o valor  .

Chamada de função editar

A chamada de função é realizada colocando o nome da função seguida de uma lista de parâmetros entre parênteses. Se a lista for vazia, deve-se colocar mesmo assim os parênteses - caso contrário, a expressão representaria a função sem realizar a chamada à mesma. Alguns exemplos de chamada de funções definidas na biblioteca padrão de C/C++:

printf("oi\n"); /* executa a função printf com o argumento "oi\n" */
printf("%i", 42); /* executa a função printf com os dois argumentos "%i" e 42 */
getchar();      /* executa a função getchar sem nenhum argumento */
exit (0);       /* executa a função exit com o argumento 0 */


Operadores de incremento, decremento e atribuição editar

C/C++ possuem uma série de operadores de atribuição que tem como objetivo tornar o código mais enxuto e, em determinados casos, permitir ou facilitar o trabalho do compilador para tirar melhor proveito das facilidades fornecidas pelo micro-processador subjacente.

Pós-incremento editar

Iniciaremos vendo o talvez mais difundido desses operadores: pós-incremento. Veja um exemplo:

int i = 0;
i++;

Temos então uma variável inteira i que tem como valor inicial  . Na segunda linha, temos um operador de pós-incremento. Esse operador faz o seguinte: lê o valor inicial de i, adiciona   a esse valor, guarda o resultado dessa soma em i e retorna o valor inicial de i. Portanto é basicamente equivalente à função seguinte:

int post_increment (int & i)
{
  int result = i;
  i = i + 1;
  return result;
}

Enfim, para concluir sobre o operador de pós-incremento, deve-se salientar que apenas pode ser aplicado a um lvalue: não faria sentido nenhum escrever 2++...

Exercício editar
  • Considere o seguinte trecho de código:
int i, j, k;
i = j = k = 0;
i = j++ = k++;

Qual o valor de cada variável ao término da execução desse trecho de código?

  • Considere o seguinte trecho de código:
int i = 0;
f(i++);

Qual o valor do parâmetro efetivo na chamada da função f?

Pré-incremento editar

O operador de pré-incremento é similar ao de pós-incremento e se escreve da mesma maneira! A única forma de diferencia-lo é na sua posição com relação ao seu argumento. Veja um exemplo:

int i = 0;
++i;

O operador de pré-incremento portanto é localizado antes do seu argumento, que deve ser um lvalue. Quando aplicado, o argumento é avaliado, o resultado dessa avaliação é somado com o número   e o resultado dessa soma é atribuído ao argumento. Esse resultado também é o resultado da expressão.

O operador de pré-incremento portanto é equivalente à seguinte função:

int post_increment (int & i)
{
  i = i + 1;
  return i;
}
Exercício editar
  • Considere o seguinte trecho de código:
int i, j, k;
i = j = k = 0;
i = ++j = ++k;

Qual o valor de cada variável ao término da execução desse trecho de código?

  • Considere o seguinte trecho de código:
int i = 0;
f(++i);

Qual o valor do parâmetro efetivo na chamada da função f?

  • Na sua opinião, qual dos dois operadores de incremento deve ser executado mais rapidamente?
Operadores de decremento editar

Existem dois operadores para decrementar um lvalue. Não surpreendemente são chamados pós-decremento e pré-decremento, tem como sintaxe -- e se distinguem pela posição relativa ao seu operando. Temos um exemplo de ambos operadores no trecho de código seguinte:

int i = 5, j = 5;
--i;
j--;

A segunda e terceira linhas contêm, respectivamente, o operador de pré-decremento aplicado à variável i e o operador de pós-decremento aplicado à variável j. O valor do primeio decremento é   e o do segundo é  .

Exercícios editar
  • Considere o seguinte trecho de código:
int i, j, k;
i = j = k = 0;
i = j-- = --k;

Qual o valor das variáveis i, j e k ao término da execução desse trecho de código?

  • Escreva funções que tem papel idêntico aos dois operadores de decremento.
Operadores de atribuição composta editar

Além do operador de atribuição simples, existem uma série de outros operadores de atribuição que correspondem ao efeito conjugado de uma operação aritmética ou binária e de uma atribuição. A seguinte tabela resume esses operadores

+= soma atribuição i += j equivale a i = i + j
*= produto atribuição i *= j equivale a i = i * j
-= subtração atribuição i -= j equivale a i = i - j
/= divisão atribuição i /= j equivale a i = i / j
%= resto atribuição i %= j equivale a i = i % j
<<= deslocamento esquerda atribuição i <<= j equivale a i = i << j
>>= deslocamento direita atribuição i >>= j equivale a i = i >> j
&= conjunção binária atribuição i &= j equivale a i = i & j
^= disjunção exclusiva binária atribuição i ^= j equivale a i = i ^ j
Exercício editar

Considere o seguinte trecho de código:

int i = 0;
i += 5;
i -= -5;
i *= 3;
i /= 2;
i %= 2;
i <<= 1;
i >>= 2;
i &= 15;
i |= (1 << 5);

Qual o valor de i após cada atribuição?

Composição seqüêncial editar

O operador , (vírgula) pode ser utilizado para combinar seqüencialmente expressões. É um operador binário infixo. A sintaxe portanto é:

exp1 , exp2

A expressão exp1 é avaliada primeiro, e a expressão exp2 é então avaliada. O resultado da segunda sub-expressão é o resultado da expressão completa. Assim, a seguinte expressão

a = 2, a += 1

tem como valor 3.

É um operador binário associativo a esquerda e pode ser repetido para encadear sequencialmente mais de duas expressões. O valor da seqüência é o valor da última expressão da seqüência. No exemplo seguinte (puramente ilustrativo...):

a = (2, 3, 5, 7)

o operador é encadeado para formar uma seqüência de quatro expressões, sendo o valor da última ( ) o valor da combinação. Nesse exemplo, a variável a é atribuída então esse valor  .

Blocos e seqüênciamento de instruções editar

Como já foi ilustrado em números exemplos, as instruções podem ser combinadas em seqüência: a ordem nas quais as instruções aparecem corresponde à ordem na qual serão executadas.

É conveniente ter uma construção para agrupar uma seqüência de instrução em um único bloco. Em Objective Caml, isso é feito com os delimitadores begin e end. Em C e C++, os blocos são delimitados por chaves:

  • { para iniciar, e
  • } para concluir.

Em C, declarações de variáveis só podem ocorrer antes da primeira instrução do bloco. Já em C++, as declarações de variáveis podem ocorrer em qualquer posição. Um exemplo de bloco C ou C++ é:

{
  int n, sq;
  scanf("%i", &n);
  sq = n * n;
  printf("%i * %i = %i\n", n, n, sq);
}

O bloco seguinte só é legal em C++ (não o é em C):

{
  int n;
  scanf("%i", &n);
  int sq = n * n;
  printf("%i * %i = %i\n", n, n, sq);
}

Um bloco pode ocorrer em qualquer posição onde uma instrução pode ocorrer, assim os blocos podem ser aninhados como no exemplo seguinte:

{
  int n;
  scanf("%i", &n);
  {
    int sq = n * n;
    printf("%i * %i = %i\n", n, n, sq);
  }
}

É importante salientar que qualquer variável declarada em um bloco é local a esse bloco. O escopo da mesma inicia logo após a sua declaração e termina no final do bloco. Desta forma, há um erro no bloco seguinte, pois a variável sq é local ao bloco mais interno e não é conhecida no bloco mais externa.

{
  int n;
  scanf("%i", &n);
  {
    int sq = n * n;
  }
  printf("%i * %i = %i\n", n, n, sq); /* atenção: erro nessa linha !!! */
}

Retorno de função editar

Como as instruções de C e C++ não retornam valor, precisamos de um mecanismo para poder indicar qual valor uma função deve retornar. É o papel da instrução de retorno. Ela tem a seguinte sintaxe:

return expressao;

onde expressao é uma expressão. A semântica é que essa expressão é avaliada e o valor resultante é o valor de retorno da função. A execução da função que contem a instrução é interrompida e o valor obtido através da avaliação da expressão é retornado.

Um exemplo editar

Segue um exemplo de uso da instrução de retorno em uma função que calcula o quadrado de um dado número decimal:

float quadrado (float x)
{
  return x * x;
}

Observação sobre a função main editar

Lembre-se que a função main é, por convenção, o ponto inicial de execução de um programa escrito em C++ ou em C. O tipo de retorno da função main é o tipo int. O valor de retorno pode ser utilizado para fornecer informações sobre o desenrolar da execução do nosso programa ao sistema operacional ou a demais programas. Por convenção nos sistemas Unix e similares, um programa retorna o valor 0 quando a execução transcorreu normalmente. Em C e C++, o comportamento defaut da função main é de retornar o valor 0. Assim, o seguinte programa:

int main ()
{
}

é equivalente ao seguinte programa

int main ()
{
  return 0;
}

Exercícios editar

Escreva uma função que, dada um número decimal, retorna o cubo desse número.

Condição editar

A execução de uma instrução, ou de um bloco de instrução, pode ser condicionada ao valor de uma condição, utilizando a tradicional construção if ... then ... else .... A sintaxe básica é a seguinte:

if (condicao) 
  instrucao1
else
  intrucao2

Note que a condição deve aparecer entre parênteses, e apenas pode haver uma ocorrência da construção sintática "instrução" em cada ramo de execução. Caso seja preciso realizar mais de uma instrução em um ramo, deve-se agrupá-las em um bloco utilizando a construção de blocos vista anteriormente.

Um primeiro exemplo de condição que permite atribuir à variável menor o menor valor de duas variáveis n e m é:

if (n <= m)
  menor = n;
else
  menor = m;

Esse segundo exemplo demostra como combinar uma instrução condicional com blocos:

if (n <= m)
{
  min = n;
  max = m;
}
else
{
  min = m;
  max = n;
}

Uma outra forma bastante comum de escrever a construção anterior, utilizando um leiaute um pouco diferente é:

if (n <= m)  {
  min = n;
  max = m;
} else {
  min = m;
  max = n;
}

Em Objective Caml, as instruções formam uma classe de expresssões e algumas restrições de tipos aplicam-se às instruções como a instrução condicional, onde as instruções da ambos ramos devem ter o mesmo tipo. Tanto em C quanto em C++, as instruções não possuem tipos, logo não existe essa restrição.

É importante salientar que o ramo else é opcional. Assim a construção sintática seguinte é perfeitamente legal:

if (condicao)
  instrucao1

Um exemplo que ilustra essa forma é

if (menor > maior) {
  int tmp = menor;
  menor = maior;
  maior = tmp;
}

Enfim, pode-se encadear um número qualquer de instruções condicionais, utilizando a seguinte forma:

if (condicao1)
  instrucao1
else if (condicao2)
  intrucao2
else if (condicao3)
  intrucao3
else
  instrucaoe

Nesse caso, o último ramo também é opcional, como demostrado no seguinte exemplo:

if (mes = 1) printf("janeiro");
else if (mes = 2) printf("fevereiro");
else if (mes = 3) printf("marco");
else if (mes = 4) printf("abril");
else if (mes = 5) printf("maio");
else if (mes = 6) printf("junho");
else if (mes = 7) printf("julho");
else if (mes = 8) printf("agosto");
else if (mes = 9) printf("setembro");
else if (mes = 10) printf("outubro");
else if (mes = 11) printf("novembro");
else if (mes = 12) printf("dezembro");

Exemplos editar

Com o operador condicional, e utilizando a recursividade, já podemos a definir algumas funções interessantes.

Esse primeiro exemplo calcula o  -ésimo elemento da seqüência de Fibonacci.

int fib (int n)
{
  if (n == 1 || n == 2)
    return 1;
  else
    return fib(n-1) + fib(n-2);
}

Esse segundo exemplo calcula   onde   representa um número decimal e   representa um número natural.

int power (float x, int n)
{
  if (n == 0)
    return 1;
  else {
    float x2 = power (x, n/2);
    float quad = x2 * x2;
    if (n % 2 == 1) {
      quad *= x;
    }
    return quad;
  }
}

Exercícios editar

  • Defina uma função que calcula o maior divisor comum de dois números naturais maiores que um.
  • Defina uma função que testa se um número positivo é primo.

Seleção editar

As linguagens C e C++ possuem um segundo tipo de instrução condicional, que identificamos como instrução de seleção. Enquanto uma instrução permite orientar o fluxo de execução em função do valor de uma condição, uma instrução de seleção permite orientar o fluxo de execução em função de um valor inteiro.

A sintaxe da instrução de seleção é:

switch (expressao) 
instrucao

com a restrição que expressao é um expressão de tipo inteiro. Em geral instrucao é um bloco de instruções (entre chaves).

A semântica é um pouco complexa a ser explicada, mas, felizmente, é mais fácil de entender!... A semântica é que expressao é avaliada, e em função do valor obtido através dessa avaliação, o fluxo de execução continua a partir de um certo ponto de controle ou após a instrução de seleção. Para efeitos de explicação, vamos assumir que   é o valor de expressão. Existe dois tipos de pontos de controle: case ou default. Os pontos casesão seguidos de uma expressão. Se existir pelo menos um ponto case tal que o valor da expressão associada seja também igual a  , então a fluxo de execução continua a partir do primeiro desses pontos. Se não existir nenhum ponto case satisfazendo essa condição, e existir um ponto default, então a execução continua a partir do ponto default. Se também não existir ponto default, a execução continua após a instrução de seleção.

Exemplos editar

Para passar a intuição, então vamos lançar mão de alguns exemplos.

  • O primeiro exemplo é:
int i = 1;
switch (i) {
  case 0: printf("0\n");
  case 1: printf("1\n");
}
printf("saiu\n");

A expressão de seleção tem valor   que também é o valor do segundo ponto de controle. A execução desse trecho de código resultará na seguinte impressão na saída padrão:

1
saiu
  • O segundo exemplo é:
int i = 2;
switch (i) {
  case 0: printf("0\n");
  case 1: printf("1\n");
}
printf("saiu\n");

A expressão de seleção tem valor   que é o valor de nenhum ponto de controle case. Como não há ponto de controle default, o fluxo de execução prossegue após a instrução de seleção. A execução desse trecho de código resultará na seguinte impressão na saída padrão:

saiu
  • O terceiro exemplo é:
int i = 2;
switch (i) {
  case 0: printf("0\n");
  case 1: printf("1\n");
  default: printf("outro\n");
}
printf("saiu\n");

A expressão de seleção tem valor   que é o valor de nenhum ponto de controle case. Como há um ponto de controle default, o fluxo de execução prossegue após esse ponto de controle. A execução desse trecho de código resultará na seguinte impressão na saída padrão:

outro
saiu
  • O quarto exemplo é:
int i = 0;
switch (i) {
  case 0: printf("0\n");
  case 1: printf("1\n");
  default: printf("outro\n");
}
printf("saiu\n");

Agora a expressão de seleção tem valor  . O primeiro ponto de controle tem valor  , e a execução prossegue a partir dele. A saída portanto será:

0
1
outro
saiu

Será que é isso que você estava esperando? Se for, muito bem, você leu atentamente os parágrafos iniciais. Se não for, talvez é porque você ainda tem em mente o comportamento da construção de casamento de padrões de Objective Caml. Nessa construção, apenas é avaliada a expressão que corresponde ao primeiro padrão que casa. Na verdade esse tipo de comportamento é mais comum em algoritmos e processamento de dados que o comportamento exibido por esse último exemplo. Vamos ver na seqüência como obtê-lo em C ou C++.

Give me a break editar

A instrução break permite interromper o fluxo de execução em uma instrução de seleção. Quando o fluxo de execução encontra uma instrução break, o fluxo sai da instrução de seleção mais interna contendo essa instrução. Continuando a linha de exemplos da seção, vejamos como empregar a instrução break:

int i = 0;
switch (i) {
  case 0: printf("0\n");
    break;
  case 1: printf("1\n");
    break;
  default: printf("outro\n");
}
printf("saiu\n");

O valor da expressão de seleção é   que também é o valor do primeiro ponto de controle case. A instrução de impressão é executada e uma instrução break é encontrada, resultando no desvio do fluxo de execução para a primeira instrução após a instrução de seleção. A execução desse trecho de código resulta então na seguinte impressão na saída padrão

0
saiu

Exemplo editar

Para concluir sobre a instrução de seleção, mostramos um exemplo completo. Esse exemplo ilustra os diferentes pontos já visto e ainda a possibilidade de enumerar diversos pontos de controle.

void imprime_numero_em_portugues (int n)
{
  switch (n) {
    case 0:
      printf("nenhum");
      break;
    case 1:
      printf("um");
      break;
    case 2: case 3:
      printf("poucos");
      break;
    case 4: case 5: case 6:
      printf("alguns");
      break;
    default:
      printf("muitos");
  }
}

Repetição editar

As linguagens C e C++ possuem três construções de repetição diferentes: instrução while, instrução do e instrução for.

Instrução while editar

A sintaxe da instrução while é a seguinte:

while (condicao)
  instrucao

A semântica é similar à da linguagem Objective Caml:

  • A expressão condicao é avaliada
  • Se o resultado dessa avaliação é   (ou  ), então termina a execução da instrução while,
  • Se o resultado dessa avaliação é   (ou diferente de  ), então a instrução instrucao é avaliada e a instrução while é executada novamente (volta-se ao primeiro ponto).

Nessa construção, o corpo da repetição é executado zero ou mais vezes.

Segue aqui um exemplo onde a instrução é empregada para calcular o n-ésimo valor da seqüência de Fibonacci.

int fibonacci (int n)
{
   if (n <= 2) return 1;
   int corrente, anterior, i;
   corrente = anterior = 1;
   i = 2;
   while (i != n) {
     i += 1;
     int tmp = anterior;
     anterior = corrente;
     corrente += tmp;  
   }
   return corrente;
}


Exercício editar

Utilizando a instrução while, escreva funções para:

  • calcular o fatorial de um número inteiro positivo;
  • elevar um número decimal à uma potência inteira;
  • calcular o maior divisor comum de dois números inteiros positivos.

Instrução do editar

A instrução do tem a seguinte sintaxe:

do
  instrucao
while (condicao);

A semântica é similar à construção while, a diferença ficando no momento que a condição de repetição é avaliada: após a execução do corpo da instrução. Assim, em uma instrução do, o corpo é executado pelo menos uma vez.

Exemplo editar

A função seguinte emprega uma instrução do e calcula o número mínimo de bits necessários para representar um número inteiro.

int logaritmo2 (int n)
{
  int result = 0;
  do {
    n /= 2;
    result += 1;
  } while (n);
  return result;
}

Instrução for editar

A instrução for é a mais complexa das construções de repetição das linguagens C e C++. A sintaxe dela é a seguinte:

for (expressao_init; condicao_c; expressao_inc)
  instrucao

onde:

  • expressao_init é uma expressão ou declaração de inicialização;
  • condicao_c é uma condição de repetição
  • expressao_inc é uma expressão de incremento
  • instrucao é a instrução que constitui o corpo da repetição.

A semântica é a seguinte. Primeiro expressao_init é avaliada. Em seguida, condicao_c é avaliada. Se o resultado da avaliação for   ou  , então a execução prossegue após a instrução for. Caso contrário, o corpo instrucao é executado. Após isso, a expressão de incremento expressao_inc é avaliada.

Em uma instância típica de uma instrução for, a expressão expressao_init é utilizada para declarar e inicializar variáveis, e expressao_inc para incrementar essas variáveis. Por exemplo, o código seguinte resulte na impressão dos 10 primeiros números naturais na saída padrão:

for (int n = 0; n < 10; ++n)
  printf("%i\n", n);

Diferente de Objective Caml, onde a construção de repetição limitada só permite incrementar ou decrementar a variável de laço, em C e C++, qualquer operação pode ser utilizada para construir expressao_inc. Assim, a função seguinte imprime todos os múltiplos de   que são menores ou iguais a topo:

int imprime_multiplos (int m, int topo)
{
  for (multiplo = m; multiplo <= topo; multiplo += m)
    printf("%d\n", multiplo);
}

Evidentemente, pode-se utilizar uma instrução bloco para combinar mais de uma instrução no corpo de uma iteração for:

int imprime_multiplos_em_uma_linha_só (int m, int topo)
{
  for (int multiplo = m; multiplo <= topo; multiplo += m) {
    printf("%d", multiplo);
    if (multiplo + m <= topo) {
      printf(" ");
    } else {
      printf("\n");
    }
}

Modificação do fluxo de execução em repetições editar

Existem duas formas de modificar o fluxo de execução no corpo de uma repetição: através de uma interrupção, ou passando para a próxima iteração.

Interrupção editar

A instrução de interrupção tem a seguinte sintaxe:

break;

Observe que é a mesma palavra-chave que utilizamos em instruções de seleção (switch). Blocos de instrução em repetições e seleção são as duas únicas possibilidades para ter uma instrução de interrupção break.

A semântica é que, quando o fluxo de execução encontra uma instrução de interrupção, ele pula diretamente para depois da instrução de repetição mais aninhada que o contem.


Próxima repetição editar

A instrução de próxima repetição ou continuação tem a seguinte sintaxe:

continue;

Essa instrução só pode ocorrer dentro do bloco de instruções de uma instrução de repetição. A semântica é que o fluxo de execução pula diretamente para o fim do corpo da repetição. Seguem três trechos esquemáticos desse comportamento

while (/* condição */) {
  /* algumas instruções */
  continue;
  /* outras instruções */
  /* alvo do pulo do continue é aqui*/
}
do {
  /* algumas instruções */
  continue;
  /* outras instruções */
  /* alvo do pulo do continue é aqui*/
} while (/* condição */);
for (/* inicialização */; /* teste */; /* incremento */) {
  /* algumas instruções */
  continue;
  /* outras instruções */
  /* alvo do pulo do continue é aqui*/
}

Exercícios editar

Defina funções que desenham as seguintes formas geométricas na saída padrão:

  • Um quadrado, de lado  
Exemplo de saída para n = 5:
*****
*   *
*   *
*   *
*****
  • Um retângulo, de largura   e altura  
Exemplo de saída para n = 5 e m = 4
*****
*   *
*   *
*****
  • Um quadrado rotacionado de 45 graus, cuja diagonal é  .
Exemplo de saída para n = 2
  *
 * *
*   *
 * *
  *