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.
- 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 0 | coluna 1 | coluna 2 | coluna 3 | |
|---|---|---|---|---|
| linha 0 | m[0][0] = 1 | m[0][1] = 2 | m[0][2] = 3 | m[0][3] = 4 |
| linha 1 | m[1][0] = 5 | m[1][1] = 6 | m[1][2] = 7 | m[1][3] = 8 |
| linha 2 | m[2][0] = 9 | m[2][1] = 10 | m[2][2] = 11 | m[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
| Forma | Resultado |
|---|---|
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 (
linhade 0 a 2); - o laço de dentro anda pelas colunas daquela linha (
colunade 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. Oif (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 noifacima.
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 fora | Laço de dentro | A 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 deint 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::arrayoustd::vectorquando o programa crescer.
Quando dá errado
#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.
& 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].
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.
$ 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
| Forma | O 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:
3. O número de colunas
Complete para obter o número de colunas de m:
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, useconst auto& linha.- Índices trocados ou fora do tamanho não são conferidos: dão lixo, em silêncio.