1차원 배열
C++ 내장 1차원 배열의 선언·초기화·인덱스·순회·크기 정보 경계를 익히고 std::array, std::vector, std::span과 선택 기준을 비교합니다.
여러 점수처럼 같은 타입의 값을 정해진 개수만큼 한 묶음으로 다룰 때 1차원 배열을 사용할 수 있습니다.
C++ 내장 배열은 단순하지만 계약이 엄격합니다. 크기는 컴파일 시 정해지고, 원소는 연속 저장되며, 인덱스 범위는 프로그래머가 지켜야 합니다.
배열 객체의 세 가지 약속
내장 배열 T name[N]은 다음 성질을 가집니다.
- 모든 원소의 타입은
T로 같습니다. N개 원소가 메모리에 연속해서 저장됩니다.- 원소 번호는
0부터N - 1까지입니다.
연속 저장은 원소 사이의 주소 관계와 임의 위치 접근을 가능하게 합니다. 특정 성능 수치나 자동 경계 검사를 보장한다는 뜻은 아닙니다.
선언과 배열 크기
T name[N];N은 std::size_t로 변환되는 상수 표현식이어야 하며 0보다 커야 합니다. constexpr 변수는 사용할 수 있지만 실행 중에 정해지는 일반 변수는 표준 내장 배열의 크기로 사용할 수 없습니다.
#include <cstddef>
constexpr std::size_t student_count = 5;
int scores[student_count]{};
double temperatures[30]{};
char grades[10]{};표준 C++에는 가변 길이 배열(VLA)이 없습니다. 일부 컴파일러 확장에 의존하지 말고 실행 중 길이가 정해진다면 std::vector를 사용합니다.
자동 저장 기간의
int raw[5];처럼 스칼라 배열을 초깃값 없이 선언하면 원소 값이 설정되지 않습니다. 값을 쓰기 전에 읽지 않도록 하고, 기본적으로int raw[5]{};처럼 초기화하는 편이 안전합니다.
리스트 초기화
중괄호는 첫 원소부터 차례대로 값을 배치합니다.
int full[5]{85, 92, 78, 95, 88};
int inferred[]{1, 2, 3, 4, 5}; // 초기값 5개로 N을 추론
int partial[5]{85, 92}; // {85, 92, 0, 0, 0}
int zeros1[5]{0}; // {0, 0, 0, 0, 0}
int zeros2[5]{}; // {0, 0, 0, 0, 0}
// int too_many[2]{1, 2, 3}; // 오류: 원소보다 초기값이 많음초깃값이 부족하면 나머지 원소는 빈 리스트에서 초기화된 것처럼 처리됩니다. int와 double 같은 산술 타입의 나머지 원소는 0이 됩니다. 빈 {}는 모든 원소를 값 초기화(value-initialize)하므로 산술 원소가 모두 0이 됩니다. 반대로 초깃값이 배열 크기보다 많으면 프로그램은 ill-formed입니다.
C++ · BUILT-IN ARRAY OBJECT
배열은 0부터 N − 1까지의 연속 원소를 다룬다
int scores[5]는 같은 타입의 원소 5개를 연속해서 소유합니다. 원소 번호는 0부터 4까지이며, 마지막 유효 인덱스는 N - 1입니다.
| 선언 | [0] |
[1] |
[2] |
[3] |
[4] |
|---|---|---|---|---|---|
int scores[5]{85, 92, 78, 95, 88}; |
85 | 92 | 78 | 95 | 88 |
int partial[5]{85, 92}; |
85 | 92 | 0 | 0 | 0 |
전체 초기화
int scores[5]{
85, 92, 78, 95, 88
};
index 0 1 2 3 4
value 85 92 78 95 88
부분 초기화
int partial[5]{85, 92};
index 0 1 2 3 4
value 85 92 0 0 0
명시하지 않은 int 원소는 0이 됩니다. 크기보다 많은 초기값은 ill-formed입니다.
읽기와 쓰기는 한 칸을 가리킨다
scores[2] // 78 읽기
scores[1] = 90; // 둘째 칸 쓰기
변경 뒤: 85 · 90 · 78 · 95 · 88
scores[0]이 첫 원소이고 scores[4]가 마지막 원소입니다.
순회 조건이 경계를 만든다
for (std::size_t i = 0;
i < std::size(scores);
++i) {
use(scores[i]);
}
for (int value : scores) {
use(value);
}
i < N은 N - 1에서 멈춥니다. scores[5]를 평가해 원소처럼 읽거나 쓰면 범위 밖 접근이며 동작이 정의되지 않습니다.
경계 검사 없음 연속 저장은 주소 관계를 보장하지만 성능 수치나 자동 범위 검사를 보장하지는 않습니다.
요소 읽기, 쓰기, 순회
name[index]는 한 원소를 가리킵니다. 첫 원소는 [0], 마지막 원소는 [N - 1]입니다.
#include <cstddef>
#include <iostream>
#include <iterator>
int main() {
int scores[5]{85, 92, 78, 95, 88};
std::cout << scores[0] << '\n'; // 85
std::cout << scores[2] << '\n'; // 78
scores[1] = 90;
for (std::size_t i = 0; i < std::size(scores); ++i) {
std::cout << scores[i] << ' ';
}
std::cout << '\n'; // 85 90 78 95 88
for (const int score : scores) {
std::cout << score << ' ';
}
}인덱스가 꼭 필요하지 않다면 범위 기반 for가 경계를 직접 적지 않아도 되어 단순합니다. 원소를 바꾸려면 for (int& score : scores)처럼 참조로 받습니다.
한 배열을 끝까지 따라가기
위 프로그램은 처음에 0번 칸의 값 85와 2번 칸의 값 78을 한 줄씩 출력합니다. scores[1]=90은 원소 하나만 바꾸므로 두 순회는 모두 85 90 78 95 88을 출력합니다. 배열의 길이는 5 그대로입니다. 원소 하나의 값을 바꾸는 것과 배열에 원소를 추가하는 것은 다른 작업입니다.
아래는 부분 초기화와 한 칸 쓰기를 더 작은 배열에서 재현합니다. 주석의 결과를 보기 전에 네 칸의 값을 적어 보세요.
#include <iostream>
#include <iterator>
int main() {
int values[4]{7, 9};
values[1] = 3;
int sum{};
for (int value : values) { sum += value; }
std::cout << std::size(values) << " " << sum << "\n";
}초기 상태는 7·9·0·0이고 대입 뒤에는 7·3·0·0이므로 4 10입니다. 마지막 유효 원소는 values[3]이며, values[4]는 합에 포함할 다음 칸이 아닙니다.
범위와 값을 따로 확인하기
대입 대상만 values[3]으로 바꾸면 출력은 무엇인가요? 함수의 매개변수가 const int values[4]일 때 sizeof(values)/sizeof(values[0])가 항상 4인지도 아래 배열-포인터 설명을 읽고 판단합니다.
원소 상태와 함수 경계 풀이
대입 뒤에는 7·9·0·3이므로 4 19입니다. 함수의 대괄호 표기는 포인터 매개변수로 조정되어 원래 길이를 보존하지 않습니다. 해당 비율은 포인터 크기와 int 크기의 비율이므로 4를 보장하지 않습니다. 포인터가 원소를 가리킨다는 사실만으로 배열의 마지막 인덱스를 복원할 수는 없습니다.
범위 밖 접근은 undefined behavior
내장 배열의 operator[]는 범위를 검사하지 않습니다. 크기가 N이면 유효한 인덱스는 0 <= i && i < N입니다.
int data[3]{10, 20, 30};
int first = data[0]; // 유효
int last = data[2]; // 유효
// int wrong = data[3]; // 평가하면 undefined behavior
// data[5] = 100; // 평가하면 undefined behaviordata + 3이라는 한 칸 뒤 포인터 자체는 만들 수 있지만 그 위치에는 원소가 없으므로 역참조할 수 없습니다. 범위 밖 접근에는 컴파일 오류, 예외, 충돌 중 어느 것도 보장되지 않습니다.
sizeof, std::size, 배열-포인터 변환
실제 배열 객체가 보이는 자리에서 sizeof(array)는 전체 배열이 차지하는 바이트 수입니다. sizeof(array[0])은 한 원소의 바이트 수이므로 다음 비율은 원소 개수입니다.
#include <cstddef>
#include <iterator>
int scores[]{85, 92, 78, 95, 88};
constexpr std::size_t count = sizeof(scores) / sizeof(scores[0]);
static_assert(count == 5);
static_assert(std::size(scores) == 5); // C++17sizeof(int)가 4바이트라는 보장은 없습니다. 위 계산은 전체 바이트를 같은 원소 한 개의 바이트로 나누므로 실제 배열에서는 구현과 관계없이 5가 됩니다. 새 코드에서는 의도가 직접 드러나는 std::size(array)를 우선할 수 있습니다.
배열 이름은 대부분의 표현식에서 첫 원소를 가리키는 포인터로 변환됩니다. 특히 함수 매개변수에 배열처럼 적은 타입은 포인터 타입으로 조정됩니다.
void inspect(const int values[5]) {
// values의 실제 매개변수 타입은 const int*
const auto pointer_bytes = sizeof(values);
// std::size(values); // 오류: values는 배열이 아님
}inspect 안의 sizeof(values)는 원래 배열 전체가 아니라 포인터의 바이트 수입니다. 따라서 배열과 원소의 sizeof 비율로 호출자의 길이를 복원할 수 없습니다.
C++ · EXTENT AT THE BOUNDARY
함수 경계에서도 배열의 길이와 소유 관계를 보존한다
sizeof(array)는 실제 배열 객체가 보이는 자리에서만 전체 바이트 수입니다. 함수 매개변수의 T values[N]는 T*로 조정되므로, API 경계에서는 크기를 타입이나 값으로 함께 전달해야 합니다.
| 표현 | 크기·소유 | 접근·용도 |
|---|---|---|
| 내장 배열 | T[N]: 컴파일 시 고정된 N > 0개의 원소를 연속 소유하는 실제 배열 객체 |
std::size와 범위 for; 범위 밖 []는 undefined behavior; 단순한 지역 고정 저장과 저수준 인터페이스 |
| 고정 컨테이너 | std::array<T, N>: 컴파일 시 고정되고 N은 타입의 일부; 연속 원소를 소유하며 값처럼 복사·대입 가능 |
size(), 범위 for, at()의 범위 검사; 일반적인 고정 크기 값 |
| 가변 컨테이너 | std::vector<int>: 실행 중 크기를 결정·변경하며 int 원소를 연속 저장하고 소유 |
size(), 범위 for, at()의 범위 검사; 입력에 따라 길이가 달라지는 값 |
| 읽기 전용 뷰 | std::span<const T>: 뷰가 길이를 함께 보관하며 연속 원소를 소유하지 않음 |
size()와 범위 for; C++20 읽기 전용 함수 경계; 사용 중 원소와 저장 공간이 유효해야 함. 소유자가 살아 있어도 재할당 등으로 무효화될 수 있음 |
T[N]
고정된 N > 0개의 원소를 연속 소유합니다. 실제 배열 자리에서는 std::size와 범위 for를 쓸 수 있지만, 범위 밖 []는 undefined behavior입니다.
std::array<T, N>
고정 크기 연속 컨테이너입니다. 값처럼 복사·대입할 수 있고 size()와 at()을 제공하지만, N은 타입의 일부이며 크기를 바꿀 수 없습니다.
std::vector<int>
실행 중 크기를 정하고 바꿀 수 있는 소유 컨테이너입니다. int 원소는 연속 저장되며 size(), 범위 for, at()을 제공합니다.
std::span<const T>
C++20의 비소유 연속 뷰입니다. 길이를 함께 전달하지만 원본의 수명을 늘리지 않으므로 사용 동안 원소와 저장 공간이 유효해야 합니다. 소유자가 살아 있어도 재할당 등으로 뷰가 무효화될 수 있습니다.
실제 배열: 전체 바이트와 원소 수
int scores[]{85, 92, 78, 95, 88};
sizeof(scores)
== 5 * sizeof(scores[0]);
std::size(scores) == 5;
sizeof(int)가 4라는 보장은 없습니다. 비율은 원소 크기와 무관하게 실제 배열에서 5가 됩니다.
함수 경계: 포인터와 길이를 분리하지 않는다
void old_api(int values[5]);
// 매개변수 타입은 int*
void print(
std::span<const int> values) {
for (int value : values) {
use(value);
}
}
sizeof(values)는 old_api 안에서 포인터 크기입니다. std::span은 원소 주소와 길이를 한 뷰로 전달합니다.
선택 기준 고정 크기 값은 std::array, 실행 중 크기는 std::vector, 소유하지 않는 API 입력은 std::span이 의도를 더 직접적으로 드러냅니다.
더 명확한 배열 대안
현대 C++에서는 소유권과 크기 결정 시점을 타입으로 드러내는 편이 좋습니다.
std::array<T, N>: 컴파일 시 고정 크기이고 연속 저장합니다. 값처럼 복사·대입할 수 있으며size()와at()을 제공합니다.N은 타입의 일부이고 크기를 바꿀 수 없습니다.std::vector<T>: 실행 중 길이를 정하고 바꿀 수 있는 소유 컨테이너입니다.std::span<T>: C++20의 비소유 연속 뷰입니다. 함수 경계에서 주소와 길이를 함께 받지만 원본의 수명을 늘리지는 않습니다.
std::array와 std::vector의 operator[]도 유효한 인덱스를 요구합니다. 범위 오류를 예외로 처리하려면 at()을 사용하며, 인덱스가 size() 이상이면 std::out_of_range를 던집니다.
#include <array>
#include <iostream>
#include <span>
#include <vector>
void print(std::span<const int> values) {
for (const int value : values) {
std::cout << value << ' ';
}
std::cout << '\n';
}
int main() {
std::array<int, 5> fixed{85, 92, 78, 95, 88};
std::vector<int> dynamic{10, 20, 30};
print(fixed);
print(dynamic);
const int last = fixed.at(4);
}고정 크기 데이터를 일반적인 값처럼 다루면 std::array, 입력에 따라 길이가 바뀌면 std::vector, 이미 소유된 연속 데이터를 함수가 잠시 보기만 하면 std::span이 자연스럽습니다.
핵심 정리
- 내장 배열의 크기는 0보다 큰 컴파일 타임 상수이며 선언 뒤 바뀌지 않습니다.
- 부분 리스트 초기화에서 남은 산술 원소는 0이 되고, 초깃값이 너무 많으면 ill-formed입니다.
- 원소는 연속 저장되고 인덱스는
0부터N - 1까지입니다. - 범위 밖 원소 접근은 undefined behavior이며 자동 진단이나 예외를 기대할 수 없습니다.
- 실제 배열에서는
std::size와 범위 기반for를 사용하고, 포인터로 조정되는 API 경계에서는 크기 정보를 별도로 보존합니다. - 목적에 따라
std::array,std::vector,std::span으로 의도를 더 명확히 표현합니다.