소년코딩

07.02 - 배열, Array (Part 2)

Initializing fixed arrays

배열 요소는 일반 변수처럼 생성될 때 초기화되지 않는다.

배열을 초기화하는 한 가지 방법은 요소별로 초기화를 하는 것이다.

int prime[5]; // hold the first 5 prime numbers
prime[0] = 2;
prime[1] = 3;
prime[2] = 5;
prime[3] = 7;
prime[4] = 11;

그러나 위 방법은 배열 요소가 많을수록 고통스럽다.

다행히 C++에서는 초기화 목록(initializer list)을 사용해서 전체 배열을 초기화하는 편리한 방법을 제공한다.

int prime[5] = { 2, 3, 5, 7, 11 }; // use initializer list to initialize the fixed array

배열에 저장할 수 있는 목록보다 많은 초기화 값이 있으면 컴파일러에서 오류가 발생한다.

그러나 배열에 저장할 수 있는 목록보다 적은 초기화 값이 있으면 나머지 요소는 0으로 초기화된다.

#include <iostream>

int main()
{
    int array[5] = { 7, 4, 5 }; // only initialize first 3 elements

    std::cout << array[0] << '\n';
    std::cout << array[1] << '\n';
    std::cout << array[2] << '\n';
    std::cout << array[3] << '\n';
    std::cout << array[4] << '\n';

    return 0;
}
/*
This prints:
7
4
5
0
0
*/

따라서 배열의 모든 요소를 0으로 초기화하려면 다음과 같이 해야 한다.

// 배열의 모든 요소를 0으로 초기화한다.
int array[5] = { }; // int array[5] = {0};

C++ 11에서는 유니폼 초기화(uniform initialization) 문법을 사용할 수 있다.

int prime[5] { 2, 3, 5, 7, 11 }; // use uniform initialization to initialize the fixed array
// note the lack of the equals sign in this syntax

Omitted length

초기화 목록(initializer list)을 고정 배열을 초기화하는 경우 컴파일러에서 배열의 길이를 알아낼 수 있으므로 명시적으로 배열 길이를 선언하지 않아도 된다.

다음 두 라인은 같다:

int array[5] = { 0, 1, 2, 3, 4 }; // 배열의 길이를 명시적으로 정의한다.
int array[] = { 0, 1, 2, 3, 4 }; // 초기화 목록을 사용하면 배열의 길이를 생략할 수 있다.

이렇게 하면 입력이 줄어들 뿐만 아니라 나중에 요소를 추가하거나 제거할 때 배열의 길이를 수정할 필요가 없다.


Arrays and enums

배열에 대한 커다란 문제 중 하나는 정수 인덱스가 프로그래머에게 의미 있는 정보가 아니라는 것이다. 학생이 5명인 경우를 생각해보자.

const int numberOfStudents(5);
int scores[numberOfStudents];
scores[2] = 76;

scores[2]가 누구의 점수일까? 분명하지 않다.

이 문제는 열거형을 설정해서 열거자들을 각 배열 인덱스에 매핑해서 해결할 수 있다.

enum StudentNames
{
    KENNY,       // 0
    KYLE,        // 1
    STAN,        // 2
    BUTTERS,     // 3
    CARTMAN,     // 4
    // JAMES
    MAX_STUDENTS // 5  // JAMES를 추가하면 6
};

int main()
{
    int scores[MAX_STUDENTS]; // allocate 5 integers
    scores[STAN] = 76;

    return 0;
}

이렇게 하면 배열의 각 요소가 나타내는 의미가 훨씬 명확해진다. StudentNames 열거형에는 MAX_STUDENTS라는 추가 열거자가 있다. 이 열거자는 배열의 길이를 나타내는 열거자로, 배열이 적절한 길이를 갖도록 배열 선언에 사용된다. 이렇게 하면 다른 열거자를 추가하면 배열의 길이가 자동으로 조정되기 때문에 유용하다.

이 트릭은 열거자 값을 수동으로 변경하지 않는 경우에만 유용하다.

Arrays and enum classes

열거형 클래스(enum class)는 암시적으로 정수로 변환되지 않는다.

그래서 다음과 같이 하면 오류가 발생한다:

enum class StudentNames
{
    KENNY, // 0
    KYLE, // 1
    STAN, // 2
    BUTTERS, // 3
    CARTMAN, // 4
    WENDY, // 5
    MAX_STUDENTS // 6
};

int main()
{
    int scores[StudentNames::MAX_STUDENTS]; // allocate 6 integers
    scores[StudentNames::STAN] = 76;
}

이 문제는 static_cast를 사용해서 열거자를 정수로 변환해 해결할 수 있다.

int main()
{
    int testScores[static_cast<int>(StudentNames::MAX_STUDENTS)]; // allocate 6 integers
    testScores[static_cast<int>(StudentNames::STAN)] = 76;
}

그러나 이렇게 하는 것은 번거로우므로 네임스페이스 내부에 표준 열거형을 사용하는 것이 좋다.

namespace StudentNames
{
    enum StudentNames
    {
        KENNY, // 0
        KYLE, // 1
        STAN, // 2
        BUTTERS, // 3
        CARTMAN, // 4
        WENDY, // 5
        MAX_STUDENTS // 6
    };
}

int main()
{
    int testScores[StudentNames::MAX_STUDENTS]; // allocate 6 integers
    testScores[StudentNames::STAN] = 76;
}

Passing arrays to functions

언뜻 보기에 함수에 배열을 전달하는 것은 일반적인 변수를 전달하는 것처럼 보이지만 C++에서는 배열을 다르게 처리한다.

일반 변수가 값으로 전달되면 C++은 인수 값을 함수 매개 변수로 복사한다. 매개 변수는 복사본이기 때문에 매개 변수의 값을 변경해도 원래 인수의 값은 변경되지 않는다.

그러나 만약 배열이 크면 배열을 복사하는 것은 매우 비싼 작업일 수 있으므로, C++은 배열이 함수로 전달될 때 배열을 복사하지 않는다. 대신 실제 배열이 전달된다. 이는 배열 요소의 값을 직접 변경할 수 있는 부작용이 있다.

다음 예제는 이 개념을 보여준다:
#include <iostream>

void passValue(int value) // value는 인수의 복사본이다.
{
    value = 99; // 여기에서 값을 변경해도 인수의 값은 변경되지 않는다. (여기서 인수는 main 함수의 value)
}

void passArray(int prime[5]) // prime은 실제 배열이다.
{
    prime[0] = 11; // 여기에서 요소의 값을 변경하면 실제 배열의 요소의 값이 바뀐다. (여기서 실제 배열은 main 함수의 prime )
    prime[1] = 7;
    prime[2] = 5;
    prime[3] = 3;
    prime[4] = 2;
}

int main()
{
    int value = 1;
    std::cout << "before passValue: " << value << "\n";
    passValue(value);
    std::cout << "after passValue: " << value << "\n";

    int prime[5] = { 2, 3, 5, 7, 11 };
    std::cout << "before passArray: " << prime[0] << " " << prime[1] << " " << prime[2] << " " << prime[3] << " " << prime[4] << "\n";
    passArray(prime);
    std::cout << "after passArray: " << prime[0] << " " << prime[1] << " " << prime[2] << " " << prime[3] << " " << prime[4] << "\n";

    return 0;
}

/*
before passValue: 1
after passValue: 1
before passArray: 2 3 5 7 11
after passArray: 11 7 5 3 2
*/

위 예제에서, passValue() 함수는 매개 변수 value가 실제 변수가 아니라 main() 함수 안에 있는 변수 value의 복사본이기 때문에 main() 함수의 변수 value의 값은 바뀌지 않는다. 그러나 함수 passArray()의 매개 변수 배열은 실제 배열이므로 passArray() 함수는 실제 배열 요소의 값을 직접 변경할 수 있다.

왜 이런 일이 일어나는지는 배열이 C++에서 구현되는 방법과 관련이 있다. 포인터(pointer)를 다룬 후에 다시 살펴보겠다.

부수적으로, 함수가 전달된 배열 요소를 수정하지 못하도록 하려면 배열을 const로 만들면 된다.

// prime이 실제 배열인 경우에도 이 함수 내에서 상수로 처리된다.
void passArray(const int prime[5])
{
    // 그래서 아래의 명령어들은 컴파일 에러를 일으킨다.
    prime[0] = 11;
    prime[1] = 7;
    prime[2] = 5;
    prime[3] = 3;
    prime[4] = 2;
}

sizeof and arrays

sizeof 연산자를 배열에서 사용할 수 있으며, 배열의 전체 크기(배열 길이에 요소 크기를 곱한 값)를 반환한다. 그러나 C++가 함수에 배열을 전달하는 방식으로 인해 함수에 전달된 배열에서는 제대로 작동하지 않는다!

void printSize(int array[])
{
    std::cout << sizeof(array) << '\n'; // 배열 크기가 아니라 포인터 크기를 출력한다!
}

int main()
{
    int array[] = { 1, 1, 2, 3, 5, 8, 13, 21 };
    std::cout << sizeof(array) << '\n'; // 배열 크기를 출력한다.
    printSize(array);

    return 0;
}
/* 
4 바이트 정수와 4 바이트 포인터가 있는 컴퓨터에서는 다음과 같이 출력된다.
32
4
*/

이러한 이유로 배열에 sizeof() 연산자를 사용하는 것에 주의해야 한다!

배열에 포함된 요소의 수를 말할 때 "길이(length)"라는 용어를 사용하고, 바이트 단위의 크기를 나타낼 때는 "크기(size)"라는 용어를 사용한다.


Determining the length of a fixed array

전체 배열 크기를 배열 요소 크기로 나누어 고정 배열의 길이를 알 수 있다.

int main()
{
    int array[] = { 1, 1, 2, 3, 5, 8, 13, 21 };
    std::cout << "The array has: " << sizeof(array) / sizeof(array[0]) << "elements\n";

    return 0;
}

This prints:
The array has 8 elements

Indexing an array out of range

길이가 N인 배열에는 배열 요소가 0에서 N-1까지 있다는 것을 기억하자. 그 범위 밖의 배열 요소에 접근하면 어떻게 될까?

int main()
{
    int prime[5]; // hold the first 5 prime numbers
    prime[5] = 13;

    return 0;
}

위 프로그램에서 배열의 길이는 5이지만, 여섯 번째 요소(인덱스 5)에 값을 저장하려고 한다.

C++은 인덱스가 배열 길이에 맞는지 확인하는 어떤 검사도 하지 않는다. 따라 위 프로그램에서 13의 값은 여섯 번째 요소가 있을 메모리에 삽입된다. 이 경우 정의되지 않은 동작(undefined behavior)이 발생한다. 예를 들어 다른 변수의 값을 덮어쓰거나 프로그램이 중단될 수 있다.

배열을 사용할 때는 배열 범위에 대해 인덱스가 유효한지 확인하자.


cpp 번역: 이 포스트의 원문은 http://www.learncpp.com/cpp-tutorial/61-arrays-part-i/ 입니다.

댓글 로드 중…

블로그 정보

소년코딩 - 소년코딩

소년코딩, 자바스크립트, C++, 물리, 게임 코딩 이야기

최근에 게시된 이야기