요약
많은 데이터는 한 줄이 아니라 표 모양을 이룬다. 학생별·시험별로 정리한 한 반의 성적, 게임판, 행렬이 그렇다. 이 수업은 2차원 구조인 표를 C++의 두 가지 형태로 소개한다. 실행할 때 크기가 정해지는 벡터의 벡터와, 컴파일할 때 크기를 아는 배열의 배열이다. 이어서 행 단위 순회와 열 단위 순회, 비용이 삼차인 행렬의 전치와 곱, 가장자리를 조심하게 만드는 칸의 이웃 칸, 그리고 메모리가 보는 방식대로 인덱스 i * columns + j를 써서 하나의 벡터에 저장한 표를 다룬다.
학습 목표
- 표를
std::vector<std::vector<T>>타입이나std::array의std::array타입으로 나타내고, 크기를 컴파일할 때 아는지에 따라 둘 중 하나를 고를 수 있다. - 표를 행 단위와 열 단위로 순회하고, 각 행과 각 열의 합계와 평균을 계산할 수 있다.
- 어떤 인덱스도 가장자리 밖으로 나가지 않도록 주의하면서 표 연산(전치, 행렬의 곱, 한 칸의 이웃 칸 세기)을 구현할 수 있다.
- 인덱스
i * columns + j를 써서 표를 평탄화한 벡터로 나타내고, 칸과 그 위치 사이의 대응을 예를 들어 설명할 수 있다.
표
행과 열
개의 행과 개의 열로 된 표는 같은 타입의 데이터 개의 모음이며, 각 데이터는 두 인덱스로 지정된다. 하나는 행 인덱스 로, 범위에 있고, 다른 하나는 열 인덱스 로, 범위에 있다. 수학은 이것을 행렬이라 부르고 형태로 쓴다. 대수학과 기하학 II 강좌는 행렬과 그 연산을 다루고, 이 수업은 그중 몇 가지를 프로그램으로 쓴다. 인덱스의 순서에는 예외가 없다. 먼저 행, 나중에 열이다. 둘을 혼동하는 것이 표에서 가장 흔한 오류이며, 두 인덱스가 같은 타입이므로 컴파일러는 이를 경고할 수 없다.
벡터 수업은 크기가 변하는 시퀀스를 소개했다. 그렇다면 표는 행의 시퀀스이고, 각 행은 다시 시퀀스이다. 곧 원소가 벡터인 벡터, std::vector<std::vector<int>> 타입이다.
#include <print>#include <vector>int main(){ std::vector<std::vector<int>> table{{1, 2, 3}, {4, 5, 6}}; std::println("{}", table); std::println("행 {}개, 열 {}개", table.size(), table[0].size()); std::println("1행: {}; 1행 2열: {}", table[1], table[1][2]); table[0][1] = 20; for (const std::vector<int>& row : table) { for (const int value : row) { std::print("{:>4}", value); } std::println(""); } const std::vector<std::vector<int>> zeros(3, std::vector<int>(4)); std::println("{}", zeros);}[[1, 2, 3], [4, 5, 6]]행 2개, 열 3개1행: [4, 5, 6]; 1행 2열: 6 1 20 3 4 5 6[[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]6번 줄은 표를 행의 목록으로 쓰며, 각 행은 자기 중괄호로 감싼다. 범위 서식은 이를 대괄호로 같은 모양으로 출력한다. table.size() 값은 행의 수이고, table[0].size() 값은 첫 행의 길이로, 제대로 된 표에서는 열의 수이다. table[1] 값은 1행 전체, 곧 하나의 std::vector<int> 객체이고, table[1][2] 값은 그 행의 원소 2이다. 이중 인덱스는 새로운 연산이 아니라 연속된 두 번의 접근이며, 첫째가 행을 고르고 둘째가 그 행 안에서 열을 고른다. 11번 줄부터 16번 줄까지는 중첩된 두 개의 범위 기반 for 반복문으로 표를 직사각형 모양으로 쓴다. 행은 회차마다 복사하지 않도록 상수 참조로 받는다. 17번 줄은 0으로 채운 3행 4열의 표를 만든다. 벡터 수업에서처럼 소괄호를 쓰면, 0 네 개로 된 행의 복사본 3개를 요청하게 된다.
3행 4열의 표를 먼저 한 행씩(i 변수는 바깥 반복문에), 그런 뒤 한 열씩(반복문을 맞바꾸어) 순회한다. 각 칸에는 방문 순서가 표시된다.
증명
표. 행 인덱스는 왼쪽에, 열 인덱스는 위쪽에 있다.
행 단위: 일 때 안쪽 반복문은 을 순회한다.
일 때 안쪽 반복문은 다시 에서 시작한다.
일 때: 표는 글을 읽듯이 한 줄씩 순회되었다.
열 단위: 일 때 안쪽 반복문은 를 순회한다.
일 때, 두 번째 열.
일 때.
일 때. 각 칸은 다시 정확히 한 번씩 방문되었다. 반복문을 맞바꾸어도 무엇을 방문하는지는 바뀌지 않고, 언제 방문하는지가 바뀐다.
길이가 다른 행
벡터의 벡터의 행들이 같은 길이여야 할 이유는 없다. 각 행은 독립된 벡터이기 때문이다. 파스칼의 삼각형처럼 바로 그것이 원하는 바일 때도 있다. 그러나 정의상 직사각형인 표에서 더 짧은 행은 데이터의 오류이며, table[0].size() 값을 열의 수로 쓰는 프로그램은 그 행 밖을 읽는다. 따라서 밖에서 들어온 표는 확인해야 한다.
#include <print>#include <vector>// 사전 조건: 없음.// 사후 조건: table에 행이 하나 이상 있고 모든// 행의 길이가 첫 행과 같으면 true를 반환한다.bool isRectangular(const std::vector<std::vector<int>>& table){ if (table.empty()) { return false; } for (const std::vector<int>& row : table) { if (row.size() != table[0].size()) { return false; } } return true;}int main(){ const std::vector<std::vector<int>> pascal{{1}, {1, 1}, {1, 2, 1}, {1, 3, 3, 1}}; const std::vector<std::vector<int>> grid{{1, 2, 3}, {4, 5, 6}}; std::println("{} {}", pascal, isRectangular(pascal)); std::println("{} {}", grid, isRectangular(grid));}[[1], [1, 1], [1, 2, 1], [1, 3, 3, 1]] false[[1, 2, 3], [4, 5, 6]] true이 함수는 행이 없는 표를 제외한다. 그런 표에는 table[0] 원소가 없다. 이 수업에서 table[0].size() 값을 읽는 함수들은 사전 조건으로 직사각형이고 비어 있지 않은 표를 요구하며, isRectangular 함수로 이를 보장할 수 있다.
배열의 배열
크기를 컴파일할 때 알면(체스판, 마방진) 표는 std::array의 std::array 타입일 수 있다. std::array<std::array<int, 3>, 3> 타입은 안쪽에서 바깥쪽으로 “정수 3개짜리 행 3개”로 읽는다. 행, 열, 대각선의 합이 모두 15인 3차 마방진을 예로 든다.
#include <array>#include <print>int main(){ constexpr std::array<std::array<int, 3>, 3> magic{{ {2, 7, 6}, {9, 5, 1}, {4, 3, 8}, }}; std::println("{}", magic); std::println("가운데 {}, {}바이트", magic[1][1], sizeof(magic));}[[2, 7, 6], [9, 5, 1], [4, 3, 8]]가운데 5, 36바이트중괄호는 양 끝에 세 개씩 있다. std::array 타입의 멤버는 C 배열 하나뿐이어서, 가장 바깥 중괄호는 std::array 타입의 것이고, 두 번째는 그 내부 배열의 것이며, 세 번째만 각 행의 것이다. 중괄호가 두 개면 첫 행이 내부 배열의 초기화자로 받아들여지고, 두 번째 행은 더 초기화할 것이 없다.
#include <array>#include <print>int main(){ const std::array<std::array<int, 3>, 2> table{{1, 2, 3}, {4, 5, 6}}; std::println("{}", table);}main.cpp: In function ‘int main()’:main.cpp:6:71: error: too many initializers for ‘const std::array<std::array<int, 3>, 2>’ 6 | const std::array<std::array<int, 3>, 2> table{{1, 2, 3}, {4, 5, 6}}; | ^Too many initializers: {4, 5, 6} 목록이 남는다. 평평한 목록 {1, 2, 3, 4, 5, 6}도 컴파일되고 행을 차례로 채우지만, 표의 모양을 감춘다. 이 강좌는 중괄호 세 겹을 쓴다.
sizeof 연산자의 36바이트는 4바이트 int 아홉 개의 크기이며(흔한 크기이지만 표준이 보장하지는 않는다), 그 밖에는 아무것도 없다. 배열의 배열은 행을 차례로 하나의 블록에 저장한다. 반면 벡터의 벡터는 행들을 담는 블록 외에 행마다 블록을 하나씩 요청하며, 그 블록들이 붙어 있을 필요는 없다. 따라서 선택은 배열 수업과 벡터 수업의 규칙을 따른다.
- 두 크기를 모두 컴파일할 때 알면 배열의 배열: 블록이 하나이고, 직사각형 모양이 타입으로 보장되며,
constexpr키워드도 쓸 수 있다. - 어느 한쪽을 실행할 때에만 알면 벡터의 벡터. 이때는 행들의 길이가 같은지 확인할 의무가 있다. 아니면 마지막 절의 평탄화한 벡터.
콘텐츠를 불러오는 중…