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 avaliai, descarta-o e indexa comj. Isso compila. Está errado. Usegrid[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ê silenciosamentegrid[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.