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)이 발생한다. 예를 들어 다른 변수의 값을 덮어쓰거나 프로그램이 중단될 수 있다.
배열을 사용할 때는 배열 범위에 대해 인덱스가 유효한지 확인하자.
번역: 이 포스트의 원문은 http://www.learncpp.com/cpp-tutorial/61-arrays-part-i/ 입니다.