CodeGym /행동 /C++ SELF /배열 소개: T arr[N]

배열 소개: T arr[N]

C++ SELF
레벨 11, 레슨 1
사용 가능

1. 소개

프로그램을 작성할 때 곧바로 하나의 숫자만으로는 부족하다는 사실을 알게 됩니다. 예를 들어 일주일 동안의 걸음 수 7개, 한 해의 지출 12개, 시험 점수 10개 같은 값을 저장해야 합니다. 물론 x1, x2, x3… 처럼 변수를 만들 수 있지만, 이는 코드가 시나리오 작가 없는 500부작 드라마처럼 되는 길입니다.

C-배열은 고정 크기의 "상자"로, 같은 타입의 N개 요소를 담고 있으며 각 요소는 번호(인덱스)로 접근할 수 있습니다.

문법: T arr[N]

표기법은 다음과 같습니다:

T arr[N];

여기서:

  • T — 요소의 타입(int, double, char 등)
  • arr — 배열 이름
  • N — 요소 개수(고정)

핵심 개념을 바로 잡아두면 좋습니다: 배열은 N개의 요소를 포함한 하나의 객체입니다. "N개의 개별 변수"가 아니라 하나의 통합된 객체입니다.

크기는 선언 시에 정해지며 변경되지 않습니다

초보자가 빠지기 쉬운 함정이 있습니다: 배열은 늘어나는 리스트처럼 보이지만 C-배열은 그렇지 않습니다. 크기는 선언 시점에 정해지며 이후에 바꿀 수 없습니다. 예를 들어 int a[5]; 라고 선언했다면 항상 5칸이고 "여섯째를 추가"하는 것은 다른 도구 없이는 불가능합니다(다른 도구는 나중에 다룹니다).

연습 문제와 실제 코드에서는 배열 크기를 매직 넘버로 쓰지 말고 컴파일 타임 상수로 보관하는 것이 좋은 습관입니다. 이때 constexpr와 타입 std::size_t가 유용합니다(크기와 인덱스는 보통 size_t에 해당합니다).

간단한 예: 주간 걸음수 추적기(오늘의 미니 프로젝트 이름은 StepStats)를 선언해보겠습니다.

#include <cstddef>
#include <iostream>

int main() {
    constexpr std::size_t DAYS = 7;
    int steps[DAYS] = {};            // 모든 요소 = 0
    std::cout << DAYS << '\n';       // 7
}

= {}에 주목하세요 — 단순한 장식이 아니라 안전장치입니다: 배열은 즉시 예측 가능한 값으로 초기화됩니다.

배열은 컴파일러에 의해 생성됩니다

배열의 길이는 프로그램 컴파일 시점에 알려져야 합니다. 다음과 같은 코드는 동작하지 않습니다:

#include <cstddef>
#include <iostream>

int main() {
    int n;
    std::cin >> n;
    int arr[n];  // ❌ 표준 C++에서는 오류
}

이는 C++가 오래된 언어이고 그 초기에는 이런 방식이 정상적이었기 때문입니다.

2. 인덱스: 0부터 N-1까지

여기서 가장 중요한 부분입니다. 많은 문제와 데드라인, 때로는 키보드 파손까지 초래하는 것이 바로 인덱스입니다.

요소가 N개인 배열에서 허용되는 인덱스는 다음과 같습니다:

  • 첫 번째 요소: arr[0]
  • 마지막 요소: arr[N - 1]

즉 인덱스 범위는 포함 범위로 0부터 N-1까지입니다.

이는 C/C++의 역사적 전통입니다: 인덱스는 "시작으로부터의 오프셋"이므로 첫 요소의 오프셋은 0이고 첫 인덱스는 0입니다.

머릿속에 고정해두기 좋은 표:

요소 수 N 첫 인덱스 마지막 인덱스
1
0
0
2
0
1
5
0
4
7
0
6

코드로 보여드리겠습니다(그리고 동시에 "마지막 요소 = arr[N]"라고 쓰지 않도록 연습합시다—그건 틀립니다):

#include <cstddef>
#include <iostream>

int main() {
    constexpr std::size_t N = 5;
    int a[N] = {1, 2, 3, 4, 5};

    std::cout << a[0] << '\n';       // 1
    std::cout << a[N - 1] << '\n';   // 5
}

3. 배열 순회: 경계는 i < N

배열이 있으면 대부분 다음 단계는 루프로 순회하는 것입니다. 여기서 유명한 실수인 "off‑by‑one"이 발생합니다: 한 칸 빗나가면 이상한 일이 생깁니다.

길이 N인 배열을 순회하는 올바른 루프 형태:

for (std::size_t i = 0; i < N; ++i) {
    // arr[i]로 작업
}

왜 < N이어야 하고 <= N가 아니라는 질문에 대한 답: arr[N] 는 더 이상 배열의 요소가 아닙니다. 마지막 요소는 arr[N - 1]입니다.

머릿속에 두기 좋은 흐름도:

flowchart LR
    A["i = 0"] --> B{"i < N ?"}
    B -- yes --> C["use arr[i]"]
    C --> D["++i"]
    D --> B
    B -- no --> E["end loop"]

StepStats 예제로 모든 7개 값을 출력해보겠습니다(현재는 모두 0입니다).

#include <cstddef>
#include <iostream>

int main() {
    constexpr std::size_t DAYS = 7;
    int steps[DAYS] = {};

    for (std::size_t i = 0; i < DAYS; ++i) {
        std::cout << steps[i] << ' ';    // 0 0 0 0 0 0 0
    }
    std::cout << '\n';
}

4. 배열 초기화와 입력

이제 초보자가 출력에서 "쓰레기"를 보며 컴퓨터가 자신을 싫어한다고 의심하게 만드는 주제로 넘어갑니다. 컴퓨터는 당신을 미워하지 않습니다. 단지 당신이 작성한 것을 문자 그대로 실행할 뿐입니다.

완전 초기화 {...}

모든 요소에 값을 주면 간단하고 깔끔합니다:

#include <cstddef>
#include <iostream>

int main() {
    constexpr std::size_t N = 3;
    int a[N] = {10, 20, 30};

    std::cout << a[1] << '\n'; // 20
}

부분 초기화 {10, 20}

지정한 값이 배열 크기보다 적으면 "꼬리"는 0으로 채워집니다. 이는 리스트 초기화의 편리한 속성으로 연습 문제에서 의지할 수 있습니다.

#include <cstddef>
#include <iostream>

int main() {
    constexpr std::size_t N = 5;
    int a[N] = {10, 20};   // 나머지는 0이 됨

    for (std::size_t i = 0; i < N; ++i) {
        std::cout << a[i] << ' ';    // 10 20 0 0 0
    }
    std::cout << '\n';
}

영(제로) 초기화 = {}

모든 요소를 0으로 만들고 싶다면 = {}를 쓰세요. 이는 예측 가능성을 확보하는 가장 좋은 방법 중 하나입니다.

#include <cstddef>
#include <iostream>

int main() {
    constexpr std::size_t N = 4;
    double x[N] = {};  // 0.0 0.0 0.0 0.0

    std::cout << x[0] << '\n'; // 0
}

초기화되지 않은 배열이 위험한 이유

중요한 경고입니다. 만약 다음과 같이 쓴다면:

int a[5];

…요소들이 반드시 0일 필요는 없습니다. 거기엔 무작위 값이 있을 수 있습니다. 이는 C++가 악의적이어서가 아니라, 배열을 만든 후 어떤 값으로 채울지 명시하지 않았기 때문입니다.

"쓰레기"를 보여주는 예시는 컴파일러와 환경에 따라 달라지기 때문에 여기서는 만들지 않겠습니다. 교육용 예시는 재현 가능해야 합니다. 규칙은 간단합니다: 배열이 "깨끗한 상태"로 시작해야 한다면 = {}를 사용하세요.

StepStats에서는 특히 논리적입니다: 사용자가 걸음 수를 입력하기 전까지 값은 0이어야 하며, "동기 부여가 되는 임의 값"으로 보이면 안 됩니다.

"N개의 값 읽기" 패턴

이제 StepStats에서 실용적인 첫 단계를 진행합니다: 일주일치 걸음 수를 입력받겠습니다.

중요한 점: std::cin으로 입력받는다고 사용자가 항상 올바른 숫자를 입력한다고 보장하진 않습니다. 오늘은 입력 오류 처리 전체를 다루지 않고 배열과 반복문의 메커니즘을 연습하는 데 집중합니다.

#include <cstddef>
#include <iostream>

int main() {
    constexpr std::size_t DAYS = 7;
    int steps[DAYS] = {};

    for (std::size_t d = 0; d < DAYS; ++d) {
        std::cin >> steps[d];        // 7개의 숫자 입력
    }
}

이제 출력도 추가해 실제로 "저장된지" 확인해보겠습니다(읽고 잊어버린 것이 아님을 확인).

#include <cstddef>
#include <iostream>

int main() {
    constexpr std::size_t DAYS = 7;
    int steps[DAYS] = {};

    for (std::size_t d = 0; d < DAYS; ++d) std::cin >> steps[d];
    for (std::size_t d = 0; d < DAYS; ++d) std::cout << steps[d] << ' ';
    std::cout << '\n';
}

5. 경계 초과는 단순한 오류보다 더 나쁩니다

경계 검사를 엄격히 하는 언어나 경계 검사를 제공하는 컨테이너에서는 보통 명확한 오류를 받지만 C-배열은 훨씬 험난합니다: 만약 arr[N] 또는 arr[999] 에 접근하면 컴파일러가 이를 잡아내지 못하는 경우가 많고, 실행 중에는 예측 불가능한 동작이 발생할 수 있습니다.

이를 UB(undefined behavior)라고 부릅니다 — 표준이 정의하지 않는 동작입니다. 평이하게 말하면: "동작할 수도, 크래시 날 수도, 데이터가 조용히 망가질 수도, 내일 고장나는 척 할 수도"입니다.

UB 주제는 매우 방대하지만 오늘은 한 가지 실용적 규칙만 기억하면 됩니다: 항상 0.. N-1 범위를 벗어나지 마세요. 그리고 예, 이는 실수로 <=를 쓴 경우도 포함합니다.

참고로 표준에서 인덱싱과 관련된 세부 사항을 별도로 다루는 것(예: "array subscripting and xvalues")만 봐도 이 연산이 겉보기만큼 단순하지 않음을 암시합니다.

6. 자주 발생하는 실수: 예시와 예방책

이제 여러분의 시간을 절약해주는 섹션입니다. 여기서의 실수는 인간적입니다: 당신이 "0이 첫 번째"이고 "N이 이미 넘음"인 세계를 처음 마주했기 때문입니다.

i <= N 대신 i < N

실수는 무해해 보입니다: "N개의 요소를 순회하려고 한다". 하지만 i <= N라면 N+1번 반복하게 되어 arr[N]에 접근하게 됩니다.

나쁜 코드(이렇게 하지 마세요):

for (std::size_t i = 0; i <= N; ++i) {
    std::cout << arr[i];
}

좋은 코드:

for (std::size_t i = 0; i < N; ++i) {
    std::cout << arr[i];
}

마지막 요소와 arr[N]의 혼동

이것은 거의 항상 "사고의 오류"입니다: 요소가 N개라면 마지막 인덱스가 N이라고 생각하기 쉽습니다. 하지만 인덱스는 0부터 시작하므로 마지막은 N - 1입니다.

StepStats에서 주(사람 기준으로 7번째)의 마지막 날 인덱스는 6입니다.

코드 곳곳의 매직 넘버

여기저기 7을 다섯 군데 쓰면, 언젠가 네 곳은 10으로 바꾸고 한 곳을 잊어버리기 쉽습니다. 전형적인 실수입니다.

대신 상수를 사용합니다:

constexpr std::size_t DAYS = 7;

그리고 모든 곳에서 이를 사용하세요.

인덱스를 int로 쓰고 생기는 이상한 비교

기술적으로는 int i = 0; i < DAYS; ++i 가 종종 "작동"합니다. 그러나 타입이 섞이면 원치 않는 암묵적 변환이 일어날 수 있습니다.

크기 타입은 보통 std::size_t이므로 루프의 인덱스도 std::size_t로 만드는 것이 합리적입니다. 이는 관습 때문이 아니라 부호 있는 타입과 부호 없는 타입을 비교하여 놀랄 일을 줄여줍니다.

7. C-배열 작업 시 흔한 실수들

실수 #1: <= 때문에 배열 경계 초과.
이 실수는 거의 맞는 것처럼 보이지만 결과는 심각할 수 있습니다. 루프가 한 번 더 돌면 배열이 아닌 메모리에 접근하게 됩니다. 결과는 "대체로 동작한다"에서부터 갑작스러운 크래시까지 다양합니다.

실수 #2: 초기화되지 않은 요소를 읽음.
만약 int a[10];를 선언하고 곧바로 a[0] 를 출력하면, 당신이 값을 정하지 않았기 때문에 읽는 값은 예측 불가능합니다. 때로는 0일 수도, 아닐 수도 있습니다. 예측 가능한 시작값이 필요하면 = {}를 사용하거나 읽기 전에 배열을 입력으로 채우세요.

실수 #3: 크기 상수 대신 매직 넘버 사용.
배열 크기를 여러 곳에 하드코딩하면 관리가 어려워집니다. constexpr std::size_t N = ...;처럼 상수로 정의하면 코드가 훨씬 안정적입니다.

실수 #4: "마지막 요소"에 대한 오해.
초보자들은 종종 arr[N]를 마지막 요소라고 생각합니다. 실제 마지막 요소는 arr[N - 1]입니다. 프로그래밍에서는 인덱스가 0부터 시작한다는 점을 항상 기억하세요.

실수 #5: int n; cin >> n; int a[n]; 형태로 배열을 "동적"으로 만들려는 시도.
표준 C++에서는 C-배열의 크기가 컴파일 시점에 알려져야 합니다. 일부 컴파일러는 확장 기능으로 이를 허용하지만 이식성이 없고 교육적 관점에서는 잘못된 접근입니다. 크기가 미리 알려져 있으면 C-배열을 쓰고, 그렇지 않으면 다른 도구를 사용하세요. 이 부분은 오늘 강의 범위를 벗어납니다.

1
과제
C++ SELF, 레벨 11, 레슨 1
잠금
첫 번째와 마지막
첫 번째와 마지막
1
과제
C++ SELF, 레벨 11, 레슨 1
잠금
리스트 뒤집기
리스트 뒤집기
1
과제
C++ SELF, 레벨 11, 레슨 1
잠금
근무표 시프트
근무표 시프트
1
과제
C++ SELF, 레벨 11, 레슨 1
잠금
주간 극값
주간 극값
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION