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
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)처럼 참조로 받습니다.
범위 밖 접근은 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으로 의도를 더 명확히 표현합니다.