Sobre arrays em C/C++. A minha visão.

Conhecendo o MaRZ há mais de 20 anos aprendi a valorizar os excelentes insights de meu amigo. De tempos em tempos topo com uma maneira de pensar diferente da minha que realmente me surpreende pela simplificação (ou convolução também) que são esclarecedoras, ou, pelo menos, me fazem pensar…

O último artigo postado aqui (este) pode parecer básico para alguns ou, dada a interpretação do padrão ratificado pelo comitê X3J11 (conhecido como ANSI C) pode parecer complicado para outros. Quero apenas apresentar a minha interpretação do padrão e estendê-la para o mais recente padrão aceito, o ISO C99 — Para quem estiver interessado, o padrão pode ser obtido clicando neste link.

Faça download do padrão e dê uma olhada no item 6.5.2.1. Lá você verá que a interpretação de meu amigo MaRZ está essencialmente correta: Ou seja, o uso de um identificador parcial para um array é convertido para um ponteiro. Mas, ao mesmo tempo, a especificação T E1[E2] é definida como tendo o primeiro elemento (E1) como sendo o ponteiro para primeiro item do array. Quer dizer, E1 é equivalente a &E1[0]. O elemento E2 tem que ser integral (inteiro).

Por isso é dito que E1[E2] é equivalente a *(E1 + E2). No caso de um array multidimensional a coisa continua do mesmo jeito. Reparem que arrays são construções auxiliares, isto é, arrays só existem de maneira unidimensional na memória (porque a memória é sequencial!). Arrays de mais de uma dimensão são arranjos sequenciais interpretados como se fossem multidimensionais. Por isso, quando você escreve:

int a[3][2] = { { 0, 1 }, { 2, 3 }, { 4, 5 } };
...
printf("%d\n", a[1][1]);

A expressão a[1][1] é traduzida, pelo compilador, para *(a + 2*i + j), onde i é a linha e j a coluna e, de acordo com a declaração, 0 ≤ i < 3 e 0 ≤ j < 3. Isso não significa que não podemos usar referências negativas para i e j, dependendo do ponteiro… O código abaixo é perfeitamente válido:

int *p = &amp;a[0][1];

p[-1] = -1; /* coloca -1 em a[0][0] */

De novo, o índice dentro dos parênteses é apenas uma forma de simplificar a aritimética de ponteiros e, como demonstrado no artigo do MaRZ, pode ser colocado fora dos parênteses:

-1[p] = -1; /* coloca -1 em a[0][0] */
*(p - 1) = -1; /* mesma coisa! */

A minha interpretação da expressão a[1], de acordo com a declaração do array acima, é a seguinte: a[1] não é, em si, um ponteiro. O subscrito é somente o offset que é adicionado ao ponteiro a. Assim, a é o ponteiro e a[1] é convertido para a expressão a + 2i, onde i é 1, deixando de lado o componente j e a “derefernciação”. Daí obtemos o ponteiro.

Qual é a diferença da interpretação de MaRZ? Em essência, nenhuma, mas ao estabelecer a equivalência entre um array e um ponteiro, de acordo com o padrão, pode causar alguma confusão citar um item do array ora como ponteiro não dereferenciado, ora no caso contrário.

Para minha cabeça é mais interessante pensar num array como tendo um ponteiro base (o identificador) e um offset (os subscritos). É só uma questão de ponto de vista! Determinar se o uso do identificador aponta para o dado ou devolve o dado em si é questão de perguntar se estamos usando todos os elementos que identificam o array ou não. Isso vale para ambas as interpretações…

Ahhh… sim… do ponto de vista da CPU e da memória (não do compilador!), tanto faz declarar o array a, acima, deste outro jeito:

int a[6] = { 0, 1, 2, 3, 4, 5 };

E usar a[2*i+j] para acessar os inteiros individuais. De fato, o código gerado pelo compilador tende a ser o mesmo…

Alguns detalhes adicionais, divergente do padrão ANSI C, é que arrays podem ser declarados com tamanho zero, segundo C99. Isso é particularmente útil em estruturas. C99 aceita a declaração de arrays com tamanho variável. C99 foi o padrão que incorporou o padrãi IEEE 754 para ponto flutuante (não padronizado em ANSI C). C99 adicionou novos tipos, como long long e padronizou algumas funções que, originalmente, não existiam na ANSI C Standard Library como, por exemplo, snprintf. Com relação aos arrays, o item 6.7.5.2 do padrão mostra algumas inovações interessantes…

Fugindo um pouco do assunto acima. Acredito que eu já tenha explicado, em outro post, a “confusão” entre arrays e ponteiros. Para mim, a “confusão” surgiu com a declaração da função main. Amabas as declarações abaixo são intercambiáveis:

int main(int argc, char *argv[])
int main(int argc, char **argv)

A primeira, o modo canônico, declara um array de ponteiros para char. A segunda, um ponteiro para ponteiro para char. No caso de main isso não faz diferença, mas, se você tentar aplicar o mesmo princípio às declarações de strings, pode enfrentar problemas. Por exemplo:

#include <stdio.h>
#include <string.h>

char *s = "Frederico Pissarra";

int main(void)
{
  char *p;

  p = strtok(s, " ");
  while (p)
  {
    printf("%s\n", p);
    strtok(NULL, " ");
  }

  return 0;
}

Ao compilar e executar, obterá o seguinte:

$ gcc -o test test.c
$ ./test
Segmentation fault

O problema é que o ponteiro s aponta para um array literal, uma constante! A função strtok tentar colocar marcas no array apontado por s e não consegue! Ao passo que, ao declarar:

char s[] = "Frederico Pissarra";

Estamos alocando espaço suficiente para copiar o array constante para o segmento de dados, que pode ser escrito. Repare que s, neste caso, continua sendo um ponteiro para o primeiro char do array…

Encerro recomendando a leitura do padrão C99… Tem algumas coisas interessantes por lá…

Anúncios

2 comentários sobre “Sobre arrays em C/C++. A minha visão.

  1. Outra sutileza:

    – Acho, o que quer dizer que não tenho certeza, que a string de inicializa é mantida de alguma forma num espaço reservado para escrita, ou mesmo na área de texto. E penso que vem daí a segfault.

    – Eu consegui, ontem, declarar um array

    char chunk[4] = { ‘0’, ‘0’, ‘0’, 0 };

    dentro de main e escrever sobre estes “zeros” aí. O que não é possível quando se declara

    char *chunk = “0000”;

    Logo se vc declarar

    char nome[5] = { ‘F’, ‘r’, ‘e’, ‘d’, 0 };

    A segfault some!

    1. De fato… acredito que tenha sido isso que eu disse…
      No primeiro caso, a string é colocada numa sessão read only. No segundo, num segumento de dados “escrevível”… Olhe só:

      char *s = "fred1";
      char t[] = "fred2";

      Ao compilar e gerar o test.s:

        .file "test.c"
        .globl  s
        .section  .rodata
      .LC0:
        .string "fred1"
      
        .data
        .align 8
        .type s, @object
        .size s, 8
      s:
        .quad .LC0
        .globl  t
        .type t, @object
        .size t, 6
      t:
        .string "fred2"
      
        .ident  "GCC: (Ubuntu/Linaro 4.6.1-9ubuntu3) 4.6.1"
        .section  .note.GNU-stack,"",@progbits

      :)

Deixe um comentário

Faça o login usando um destes métodos para comentar:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s