Menu

Arrays 2D em C: arrays multidimensionais, layout na memória e matrizes

Como declarar, inicializar e percorrer arrays 2D em C, o que o layout em ordem de linhas realmente significa na memória, por que passar um array 2D para uma função exige a quantidade de colunas, e um exemplo trabalhado com matrizes.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

Um array multidimensional é um array cujos elementos são eles próprios arrays. int grid[3][4]; não é um tipo especial de grade - são três elementos, cada um sendo um array de quatro ints, armazenados um atrás do outro. Quando isso faz clique, o resto do assunto vem junto: o layout, a aritmética de indexação e a regra, de outro modo desconcertante, sobre passá-los para funções.

Duas dimensões cobrem quase todo uso prático - grades, tabelas, matrizes, tabuleiros, imagens - então é com isso que esta página trabalha.

Declarando e inicializando

int grid[3][4];        // 3 linhas, 4 colunas - 12 ints

O primeiro número é quantas linhas, o segundo quantas colunas. Os inicializadores podem ser escritos em sequência ou com chaves internas; vale usar as chaves porque elas mostram o formato.

A forma int e[][3] importa: você pode deixar a quantidade de linhas em branco e deixar o inicializador decidir, mas a quantidade de colunas nunca é opcional. A próxima seção explica por quê.

Layout em ordem de linhas

O C armazena um array 2D em ordem de linhas (row-major): a linha 0 inteira, depois a linha 1 inteira, e assim por diante, em um bloco de memória ininterrupto. Não existe um array de ponteiros de linha por trás dos bastidores.

int grid[3][4] = {{ 1, 2, 3, 4},
                  { 5, 6, 7, 8},
                  { 9,10,11,12}};

como você imagina             como ela realmente fica na memória
+----+----+----+----+
|  1 |  2 |  3 |  4 |   linha 0 +--+--+--+--+--+--+--+--+--+--+--+--+
+----+----+----+----+           | 1| 2| 3| 4| 5| 6| 7| 8| 9|10|11|12|
|  5 |  6 |  7 |  8 |   linha 1 +--+--+--+--+--+--+--+--+--+--+--+--+
+----+----+----+----+            \__linha 0__/\__linha 1__/\_linha 2_/
|  9 | 10 | 11 | 12 |   linha 2
+----+----+----+----+           grid[i][j] está no índice de elemento i*4 + j

Essa fórmula, i * colunas + j, é o mecanismo inteiro - e é por isso que o compilador precisa saber a quantidade de colunas para indexar qualquer coisa. A quantidade de linhas nunca entra no cálculo.

Você pode ver o layout diretamente imprimindo endereços:

Os endereços sobem de sizeof(int) em sizeof(int) sem lacunas, inclusive onde uma linha termina e a próxima começa. O laço achatado prova isso - flat[k] percorre os doze elementos como uma sequência única.

Esse layout também tem uma consequência de desempenho que vale conhecer: percorrer linhas-e-depois-colunas toca a memória em ordem, o que a cache da CPU aprecia. Inverter o aninhamento dos laços para que o laço interno desça uma coluna salta uma linha inteira a cada vez e pode rodar várias vezes mais devagar em um array grande.

Laços aninhados

Duas dimensões pedem dois laços for: o externo escolhe a linha, o interno varre as colunas daquela linha.

Nomeie os contadores pelo que eles significam (i/row para linhas, j/col para colunas) e mantenha a ordem consistente - grid[row][col] em todo lugar. Metade de todos os bugs com arrays 2D é um par de índices trocado.

Os tamanhos em #define também não são decoração: os limites do laço e a declaração agora não podem se desencontrar quando você mudar o formato.

Passando um array 2D para uma função

Aqui está a regra que derruba todo mundo: o parâmetro da função precisa declarar a quantidade de colunas.

A razão é o decaimento. Passar grid o converte em um ponteiro para seu primeiro elemento - e seus elementos são linhas, então o tipo é int (*)[4]: ponteiro para um array de 4 ints. Para grid[i][j] significar alguma coisa, o compilador precisa saber quão longe fica uma linha, e isso é o 4. A quantidade de linhas está genuinamente ausente do tipo, e é por isso que ela viaja como um argumento separado.

Note que int (*grid)[COLS] e int grid[][COLS] são o mesmo parâmetro escrito de dois jeitos - os parênteses são obrigatórios, já que int *grid[COLS] seria um array de ponteiros. Essa distinção é coberta em ponteiros e arrays.

Se a quantidade de colunas só é conhecida em tempo de execução, os parâmetros de tipo variável do C99 permitem passá-la primeiro:

void print_any(int rows, int cols, int grid[rows][cols]);

rows e cols precisam ser declarados antes do parâmetro de array que os usa. Onde isso não estiver disponível, a alternativa comum é um array 1D plano mais aritmética manual de índices:

data[i * cols + j] é exatamente o que o compilador escreve por você no caso de tamanho fixo. Fazer isso à mão custa uma linha e funciona para qualquer formato decidido em tempo de execução.

Um exemplo com matrizes

A multiplicação de matrizes junta a página inteira - três laços aninhados sobre um armazenamento em ordem de linhas.

Dois detalhes que vale copiar. O laço interno k pareia a[i][k] com b[k][j] - um índice percorre uma linha, o outro percorre uma coluna. E a transposição começa seu laço interno em j = i + 1: começar em 0 trocaria cada par duas vezes e deixaria a matriz inalterada.

Três dimensões e além

O padrão se estende, e a regra sobre parâmetros de função também - toda dimensão exceto a primeira precisa ser declarada.

Na prática, três dimensões é onde arrays de tamanho fixo começam a ficar desajeitados, e a maior parte do código passa para um bloco plano com índices calculados ou para um array de structs que dá nome ao que cada eixo significa.

Erros comuns

  • Escrever grid[i, j]. O operador vírgula avalia i, descarta-o e indexa com j. Isso compila. Está errado. Use grid[i][j].
  • Trocar os índices. grid[col][row] lê um elemento real do lugar errado, então não há erro para te pegar. Mantenha a ordem [row][col] em todo lugar.
  • Omitir o tamanho das colunas em um parâmetro. void f(int grid[][]) não compila, e isso é o compilador te salvando.
  • Sair dos limites. Como em qualquer array, não há verificação de limites. grid[0][5] em uma grade [3][4] lê silenciosamente grid[1][1], porque o layout é contíguo e a aritmética não se importa.

Perguntas frequentes

Como se declara um array 2D em C?

Dê dois tamanhos entre colchetes: int grid[3][4]; declara 3 linhas de 4 colunas - 12 ints no total. Leia como "um array de 3 coisas, cada uma sendo um array de 4 ints", que é literalmente como o C o armazena.

Como um array 2D é armazenado na memória em C?

Em ordem de linhas (row-major): todos os elementos da linha 0, depois todos os da linha 1, e assim por diante, em um único bloco contíguo. grid[i][j] fica no deslocamento de i * colunas + j elementos a partir do início, e é por isso que a quantidade de colunas é o número de que o compilador precisa.

Como se passa um array 2D para uma função em C?

O parâmetro precisa declarar a quantidade de colunas: void print(int grid[][4], int rows) ou, equivalentemente, void print(int (*grid)[4], int rows). A quantidade de linhas pode ser omitida porque o array decai para um ponteiro para uma linha - mas sem o tamanho das colunas o compilador não consegue calcular onde uma linha começa.

Dá para inicializar um array 2D com zeros?

Sim: int grid[3][4] = {0}; zera todo elemento, porque qualquer elemento que você não liste é inicializado com zero. int grid[3][4] = {{1, 2}}; define as duas primeiras entradas da linha 0 e deixa os outros dez em zero.

Coddy programming languages illustration

Aprenda a programar com o Coddy

COMEÇAR