C++ Arrays Multidimensionais

Um array é uma fileira de valores. Mas muita coisa do mundo real é uma tabela: as notas de cada aluno em cada prova, as casas de um tabuleiro, os assentos de um cinema (fileira e número), uma planilha. Para isso, o C++ permite arrays com duas dimensões (linhas e colunas), chamados de matrizes, ou até mais.

Você vai aprender
  • Como criar uma matriz: int m[3][4] (3 linhas, 4 colunas).
  • Como ler e trocar um elemento com m[linha][coluna].
  • Como percorrer a matriz inteira com dois for, um dentro do outro.
  • Como descobrir o número de linhas e de colunas com std::size.
  • Como montar um tabuleiro de jogo da velha e verificar se alguém venceu.
  • Como calcular a média de cada aluno (por linha) e de cada prova (por coluna).

Os programas foram compilados com o g++ 15 na máquina fictícia da Ana, na pasta ~/cpp, com g++ -std=c++23 -Wall -Wextra arquivo.cpp -o programa. Esta aula usa muito o que você viu em Arrays e em Laços Aninhados.


A ideia: os assentos do cinema

No ingresso do cinema está escrito "Fileira C, assento 7". São dois números para achar um lugar: primeiro a fileira, depois a posição dentro dela. Uma matriz funciona igual:

  • o primeiro índice escolhe a linha (a fileira);
  • o segundo escolhe a coluna (o assento dentro da fileira).

Na verdade, uma matriz em C++ é um array de arrays: int m[3][4] é "um array de 3 elementos, e cada elemento é um array de 4 int". Cada linha é um array comum. Essa ideia explica tudo o que vem a seguir.


1. Criar uma matriz

#include <iomanip>
#include <iostream>

int main() {
    int m[3][4] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };
    std::cout << "Linha 1, coluna 2: " << m[1][2] << '\n';

    for (int linha = 0; linha < 3; linha++) {
        for (int coluna = 0; coluna < 4; coluna++) {
            std::cout << std::setw(4) << m[linha][coluna];
        }
        std::cout << '\n';
    }
}
int
O tipo de cada elemento, como num array comum.
m
O nome da matriz.
[3]
Quantas linhas.
[4]
Quantas colunas em cada linha. Total: 3 × 4 = 12 elementos.
{ {…}, {…}, {…} }
Os valores iniciais: um par de chaves por linha, dentro de um par de chaves externo. Escrever uma linha da matriz por linha do código deixa tudo legível.
m[1][2]
Linha 1, coluna 2. Como sempre em C++, os dois índices começam do 0: é a segunda linha, terceira coluna, o 7.
$ g++ -std=c++23 -Wall -Wextra matriz.cpp -o matriz
$ ./matriz
Linha 1, coluna 2: 7
   1   2   3   4
   5   6   7   8
   9  10  11  12

O mapa dos índices

coluna 0coluna 1coluna 2coluna 3
linha 0m[0][0] = 1m[0][1] = 2m[0][2] = 3m[0][3] = 4
linha 1m[1][0] = 5m[1][1] = 6m[1][2] = 7m[1][3] = 8
linha 2m[2][0] = 9m[2][1] = 10m[2][2] = 11m[2][3] = 12

A regra para nunca confundir: sempre [linha][coluna], nessa ordem, como "fileira, assento". O último elemento de uma matriz [3][4] é m[2][3].

Outras formas de criar

FormaResultado
int m[3][4] = {};Tudo 0. A forma recomendada de começar "em branco".
int m[2][3] = {{1, 2}, {4}};O que falta em cada linha vira 0: 1 2 0 / 4 0 0.
int m[][3] = {{1, 2, 3}, {4, 5, 6}};O número de linhas pode ser omitido (o compilador conta: 2). O de colunas, não.
int m[3][4];Sem valores: lixo, como no array comum. Evite.

2. Percorrer com dois for

O programa acima já mostrou o padrão, que é o mesmo da tabuada de Laços Aninhados:

  • o laço de fora anda pelas linhas (linha de 0 a 2);
  • o laço de dentro anda pelas colunas daquela linha (coluna de 0 a 3);
  • m[linha][coluna] é o elemento da vez;
  • o '\n' entre o fim do laço de dentro e o fim do de fora termina cada linha.

O std::setw(4) (de iomanip) dá 4 espaços para cada número, para as colunas ficarem alinhadas.

Quantas linhas e quantas colunas: std::size

Como a matriz é um array de arrays, std::size(m) dá o número de linhas (quantos arrays há dentro), e std::size(m[0]) dá o de colunas (o tamanho da primeira linha):

#include <iostream>
#include <iterator>

int main() {
    int m[3][4] = {};
    std::cout << "Linhas: " << std::size(m) << '\n';
    std::cout << "Colunas: " << std::size(m[0]) << '\n';
}
$ g++ -std=c++23 -Wall -Wextra tamanho.cpp -o tamanho
$ ./tamanho
Linhas: 3
Colunas: 4

Na prática, o mais claro é guardar os dois tamanhos em constantes (constexpr int LINHAS = 3;), como você vai ver no exemplo das notas.

Sem índices: for de intervalo

Se você não precisa saber em que linha e coluna está, dá para usar o for de intervalo duas vezes. O de fora entrega uma linha inteira (um array) por volta; o de dentro percorre essa linha:

#include <iostream>

int main() {
    int m[2][3] = {{1, 2, 3}, {4, 5, 6}};
    for (const auto& linha : m) {
        for (int x : linha) {
            std::cout << x << ' ';
        }
        std::cout << '\n';
    }
}
$ g++ -std=c++23 -Wall -Wextra rangefor.cpp -o rangefor
$ ./rangefor
1 2 3
4 5 6 

O & em const auto& linha é obrigatório aqui, não só uma economia. Sem ele, a linha "decai" para um ponteiro, perde o tamanho (como na aula Arrays), e o laço de dentro não compila. O erro está em Quando dá errado.

Missão: com dois laços, some todos os elementos da matriz e mostre Soma: 78.

#include <iostream>

int main() {
    int m[3][4] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };
    int soma = 0;
    // some os elementos aqui

    std::cout << "Soma: " << soma << '\n';
}
#include <iostream>

int main() {
    int m[3][4] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };
    int soma = 0;
    for (int linha = 0; linha < 3; linha++) {
        for (int coluna = 0; coluna < 4; coluna++) {
            soma += m[linha][coluna];
        }
    }
    std::cout << "Soma: " << soma << '\n';
}

3. Um tabuleiro de jogo da velha

O jogo da velha é uma matriz 3 × 3 de char: cada casa guarda 'X', 'O' ou um espaço ' ' (casa vazia). Jogar é atribuir a uma casa; mostrar o tabuleiro é percorrer as linhas:

#include <iostream>

int main() {
    char tabuleiro[3][3] = {
        {' ', ' ', ' '},
        {' ', ' ', ' '},
        {' ', ' ', ' '}
    };

    tabuleiro[1][1] = 'X';
    tabuleiro[0][0] = 'O';
    tabuleiro[1][0] = 'X';
    tabuleiro[2][2] = 'O';
    tabuleiro[1][2] = 'X';

    for (int linha = 0; linha < 3; linha++) {
        std::cout << ' ' << tabuleiro[linha][0] << " | "
                  << tabuleiro[linha][1] << " | "
                  << tabuleiro[linha][2] << '\n';
        if (linha < 2) {
            std::cout << "---+---+---\n";
        }
    }

    for (int linha = 0; linha < 3; linha++) {
        if (tabuleiro[linha][0] != ' ' &&
            tabuleiro[linha][0] == tabuleiro[linha][1] &&
            tabuleiro[linha][1] == tabuleiro[linha][2]) {
            std::cout << tabuleiro[linha][0] << " venceu na linha " << linha + 1 << "!\n";
        }
    }
}
$ g++ -std=c++23 -Wall -Wextra velha.cpp -o velha
$ ./velha
 O |   |
---+---+---
 X | X | X
---+---+---
   |   | O
X venceu na linha 2!
  • As jogadas: tabuleiro[1][1] = 'X'; é "X no centro" (linha do meio, coluna do meio). tabuleiro[0][0] é o canto de cima à esquerda; tabuleiro[2][2], o de baixo à direita.
  • O desenho: aqui o laço de dentro foi "desenrolado": como cada linha tem só 3 casas, elas foram escritas uma a uma, com " | " entre elas. O if (linha < 2) desenha a linha divisória só entre as linhas, não depois da última.
  • A vitória: uma linha vence se a primeira casa não está vazia e as três casas são iguais. O && ("e", veja Operadores Lógicos) junta as três condições. Uma linha de código pode continuar na seguinte, como no if acima.

Para um jogo completo, faltaria ler as jogadas com std::cin num laço, conferir as colunas e as diagonais e alternar os jogadores. A conferência das colunas é a missão final desta aula.


4. Notas por aluno × prova

A planilha da Ana: 3 alunos (as linhas) e 4 provas (as colunas). Ela quer duas coisas: a média de cada aluno e a média da turma em cada prova. Os nomes ficam num array comum, na mesma ordem das linhas:

#include <iostream>
#include <string>

int main() {
    constexpr int ALUNOS = 3;
    constexpr int PROVAS = 4;
    std::string nomes[ALUNOS] = {"Bia", "Caio", "Davi"};
    double notas[ALUNOS][PROVAS] = {
        {7.0, 8.5, 6.0, 9.0},
        {5.0, 6.5, 7.5, 6.0},
        {9.5, 9.0, 10.0, 8.5}
    };

    for (int a = 0; a < ALUNOS; a++) {
        double soma = 0.0;
        for (int p = 0; p < PROVAS; p++) {
            soma += notas[a][p];
        }
        std::cout << nomes[a] << ": média " << soma / PROVAS << '\n';
    }

    for (int p = 0; p < PROVAS; p++) {
        double soma = 0.0;
        for (int a = 0; a < ALUNOS; a++) {
            soma += notas[a][p];
        }
        std::cout << "Prova " << p + 1 << ": média da turma " << soma / ALUNOS << '\n';
    }
}
$ g++ -std=c++23 -Wall -Wextra notas.cpp -o notas
$ ./notas
Bia: média 7.625
Caio: média 6.25
Davi: média 9.25
Prova 1: média da turma 7.16667
Prova 2: média da turma 8
Prova 3: média da turma 7.83333
Prova 4: média da turma 7.83333

Compare os dois blocos de laços. Os dois usam notas[a][p] (sempre linha, coluna), mas trocam qual laço fica por fora:

Quero a média de…Laço de foraLaço de dentroA soma zera…
Cada aluno (cada linha)alunos (a)provas (p)a cada aluno
Cada prova (cada coluna)provas (p)alunos (a)a cada prova

O acumulador soma nasce dentro do laço de fora: assim ele recomeça do 0 para cada aluno (ou prova). Se nascesse antes dos dois laços, a média do Caio incluiria as notas da Bia.


Formas diferentes e qual usar

  • Mais dimensões: int cubo[2][3][4] funciona do mesmo jeito (um array de matrizes), com três índices e três laços. É raro passar de duas.
  • Com std::array: std::array<std::array<int, 4>, 3> m; é a versão moderna de int m[3][4], com .size() e .at() (veja std::array).
  • Tamanho só conhecido ao rodar: um std::vector<std::vector<int>> (um vetor de vetores, veja std::vector) cresce como você quiser e permite linhas de tamanhos diferentes.
  • Em funções: uma matriz estilo C perde o número de linhas ao ir para uma função (como o array comum), e o de colunas precisa ser fixo no parâmetro. Mais um motivo para preferir std::array ou std::vector quando o programa crescer.

Quando dá errado

Lixo na saída: linha e coluna trocadas
#include <iostream>

int main() {
    int m[2][3] = {{1, 2, 3}, {4, 5, 6}};
    for (int linha = 0; linha < 3; linha++) {
        for (int coluna = 0; coluna < 2; coluna++) {
            std::cout << m[linha][coluna] << ' ';
        }
        std::cout << '\n';
    }
}
$ g++ -std=c++23 -Wall -Wextra trocado.cpp -o trocado
$ ./trocado
1 2
4 5
-1361125376 -1985207981 

O que aconteceu: a matriz tem 2 linhas e 3 colunas, mas os laços usaram 3 linhas e 2 colunas. O 3 sumiu, o 6 também, e a "linha 2", que não existe, mostrou lixo da memória: é o acesso fora do intervalo, comportamento indefinido, sem aviso nenhum.

Como consertar: use constantes com nomes claros (LINHAS, COLUNAS) nos laços e na declaração, e teste com -g -fsanitize=address.

"‘begin’ was not declared in this scope" (faltou o & no for de intervalo)
#include <iostream>

int main() {
    int m[2][3] = {{1, 2, 3}, {4, 5, 6}};
    for (auto linha : m) {
        for (int x : linha) {
            std::cout << x << ' ';
        }
        std::cout << '\n';
    }
}
$ g++ -std=c++23 -Wall -Wextra semref.cpp -o semref
semref.cpp: In function ‘int main()’:
semref.cpp:6:22: error: ‘begin’ was not declared in this scope
    6 |         for (int x : linha) {
      |                      ^~~~~
semref.cpp:6:22: note: suggested alternatives:     « (umas 20 linhas de sugestões cortadas)
semref.cpp:6:22: error: ‘end’ was not declared in this scope

O que significa: o for de intervalo precisa saber onde a lista começa (begin) e termina (end). Com auto linha (sem &), cada linha é copiada como ponteiro, que não sabe onde termina. As sugestões do compilador (std::begin…) não ajudam aqui.

Como consertar: for (const auto& linha : m) (ou auto&, se for alterar).

m[1, 2] no lugar de m[1][2]
#include <iostream>

int main() {
    int m[2][3] = {{1, 2, 3}, {4, 5, 6}};
    std::cout << m[1, 2] << '\n';
}
$ g++ -std=c++23 -Wall -Wextra virgula.cpp -o virgula
virgula.cpp: In function ‘int main()’:
virgula.cpp:5:19: warning: top-level comma expression in array subscript changed meaning in C++23 [-Wcomma-subscript]
    5 |     std::cout << m[1, 2] << '\n';
      |                   ^
$ ./virgula
0x7ffe09c5be98

O que significa: quem vem de Python, Pascal ou de planilhas escreve m[1, 2]. Para arrays do C++, isso não é "linha 1, coluna 2". O g++ ainda aceita, com o aviso "a expressão com vírgula no índice mudou de significado no C++23", e lê como m[2]: a linha 2, que nem existe, mostrada como endereço. O Clang do editor recusa: error: type 'int[2][3]' does not provide a subscript operator.

Como consertar: um par de colchetes por dimensão: m[1][2].

"too many initializers for ‘int [2][3]’"
int main() {
    int m[2][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
    return m[0][0];
}
$ g++ -std=c++23 -Wall -Wextra demais.cpp -o demais
demais.cpp: In function ‘int main()’:
demais.cpp:2:51: error: too many initializers for ‘int [2][3]’
    2 |     int m[2][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
      |                                                   ^

O que significa: a matriz tem 2 linhas, mas recebeu 3 grupos de valores. No Clang: error: excess elements in array initializer.

Como consertar: confira as dimensões: [linhas][colunas], e cada {…} interno é uma linha.

"must have bounds for all dimensions except the first"
$ g++ -std=c++23 -Wall -Wextra semcol.cpp -o semcol
semcol.cpp: In function ‘int main()’:
semcol.cpp:2:9: error: declaration of ‘m’ as multidimensional array must have bounds for all dimensions except the first
    2 |     int m[][] = {{1, 2}, {3, 4}};
      |         ^
semcol.cpp:3:12: error: ‘m’ was not declared in this scope
    3 |     return m[0][0];
      |            ^

O que significa: "a declaração de uma matriz precisa dos tamanhos de todas as dimensões, menos a primeira". O compilador conta as linhas sozinho, mas precisa saber o tamanho de cada linha. O segundo erro é consequência do primeiro (como m não foi criada, ela "não existe").

Como consertar: int m[][2] = {{1, 2}, {3, 4}}; ou, mais claro, int m[2][2].


Referência

FormaO que faz
int m[L][C] = {{…}, {…}};Matriz de L linhas e C colunas.
int m[L][C] = {};Tudo 0.
m[i][j]Linha i (0 a L−1), coluna j (0 a C−1). Sem conferência.
std::size(m) / std::size(m[0])Número de linhas / de colunas.
for (int i…) { for (int j…) { m[i][j] } '\n'; }Percorrer linha por linha.
for (const auto& linha : m) for (int x : linha)Percorrer sem índices (o & é obrigatório).

Exercícios

1. Quantos elementos?

Quantos int cabem em int m[4][5]?

2. O canto de baixo

Numa matriz int m[4][5], complete os índices do elemento do canto de baixo à direita:

m[][]

3. O número de colunas

Complete para obter o número de colunas de m:

std::size()

4. Em int m[3][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};, quanto vale m[2][1]?

5. Para calcular a média de cada prova (cada coluna) de notas[ALUNOS][PROVAS], o laço de fora deve percorrer…

6. Qual destas declarações é válida?

Missão final: vitória na coluna

Missão: neste tabuleiro, o X completou a terceira coluna. Escreva um for que passa pelas 3 colunas e, para cada uma, confere se a casa de cima não está vazia e se as três casas da coluna (linhas 0, 1 e 2) são iguais. Se forem, mostre X venceu na coluna 3! (use coluna + 1). Dica: é o teste das linhas com os índices trocados.

#include <iostream>

int main() {
    char tabuleiro[3][3] = {
        {'O', ' ', 'X'},
        {' ', 'O', 'X'},
        {' ', ' ', 'X'}
    };
    // confira as colunas aqui

}
#include <iostream>

int main() {
    char tabuleiro[3][3] = {
        {'O', ' ', 'X'},
        {' ', 'O', 'X'},
        {' ', ' ', 'X'}
    };
    for (int coluna = 0; coluna < 3; coluna++) {
        if (tabuleiro[0][coluna] != ' ' &&
            tabuleiro[0][coluna] == tabuleiro[1][coluna] &&
            tabuleiro[1][coluna] == tabuleiro[2][coluna]) {
            std::cout << tabuleiro[0][coluna] << " venceu na coluna " << coluna + 1 << "!\n";
        }
    }
}

Resumo

  • Uma matriz é um array de arrays: int m[3][4] tem 3 linhas de 4 colunas.
  • Acesse com m[linha][coluna], sempre nessa ordem, os dois a partir do 0.
  • Inicialize com uma chave por linha: {{1, 2}, {3, 4}}, ou tudo zerado com {}.
  • Percorra com dois for: o de fora nas linhas, o de dentro nas colunas. Trocar qual fica por fora muda o que você calcula (por aluno × por prova).
  • std::size(m) dá as linhas; std::size(m[0]), as colunas. No for de intervalo, use const auto& linha.
  • Índices trocados ou fora do tamanho não são conferidos: dão lixo, em silêncio.