배열
Array
- 데이터 형이 같은 여러 값들을 연속적으로 저장할 수 있는 Data Structure(자료 구조)이다.
- 배열에서 각 값은 배열 Element(원소)라는 개별 공간에 저장된다.
- Composite Data Type(복합 데이터형)으로 분류된다.
- 배열의 선언에 필요한 세 가지 요소는 아래와 같다.
1. 각 원소에 저장될 값의 데이터형
2. 배열의 이름
3. 배열 원소의 개수 (값, 기호상수, 상수수식을 이용하여 표현한다.)
// Ex. 배열 선언의 일반형
typeName arrayName[arraySize]; // form
// 실 사용 예시
int months[12]; // example
int months[MONTH]; // example for symbolic constant
int months[3 * sizeof(int)]; // example for constant formula
Index (Subscript; 인덱스)
- 배열 원소에 차례대로 매겨지는 일련의 번호이다.
- 배열 원소에 개별적으로 접근하는 것을 용이하게 만들어 준다.
- 첫 번째 원소의 인덱스 값은 0이다.
(프로그래밍 언어별로 시작 인덱스 값은 차이가 있으며, C++은 0을 포함하는 자연수 범위로 한정된다.)
- 마지막 원소의 인덱스 값은 \(\texttt{arraySize - 1}\) 이다. (0부터 시작하기 때문에 1을 빼야 한다.)
※ 컴파일러는 사용자가 적절한 인덱스 값을 부여했는지 따로 검사하지 않는다.
int arr[10] = {};
arr[-2] = 13;
- 위 코드는 실제로 컴파일 에러를 야기하지 않는다.
\(\texttt{arr[-2] = 13}; \iff \texttt{*(arr - 2) = 13;}\) 이며, 정의되지 않은 배열의 외부 영역에 데이터를 저장하는 꼴이된다.
- C++은 메모리 범위 밖 에러에 대하여 체크하지 않는다!
- 컴파일러 입장에서는, 특정 배열이 실질적으로 몇 개의 원소를 담는지 알 수 없고, 당연히 적절한 인덱스 크기를 계산할 수도 없다.
- 프로그래머는 자체적인 판단을 통해 배열에 적절한 인덱스 값을 부여해서 메모리 낭비 및 버그를 최소화해야 한다.
Initialization for Array (배열의 초기화)
- 배열이 선언된 후, 각각의 원소들은 그 메모리 위치에 전부터 우연히 있었던 값 (Garbage Value; 쓰레기 값)을 취할 가능성이 있으므로, 배열은 선언된 직후 초기화를 필요로 한다.
- 초기화를 하지 않아 생기는 Logic Error(논리 오류)를 방지하기 위해서라도 프로그래머에게는 배열 선언 직후에 초기화하는 습관을 들여야 한다.
* 배열 초기화 문법
- 인덱스를 이용하여 개별 원소에 값을 대입하는 방식으로의 초기화는 코드 흐름 중 언제 어디서나 가능하다.
- 예외적으로, 값들을 일괄적으로 배열에 대입하는 것은 배열 선언 당시에만 가능하다.
// Ex. 배열 초기화 예시
int cards[4] = {3, 6, 8, 10};
// 첫 원소 부터 차례대로 지정된 값으로 초기화
float GPA[4] = {4.5, 3.7};
// 처음 두 개의 원소만 각각의 값으로 초기화되고 나머지 원소는 0으로 초기화
short hand[5] = {0};
// hand[0] 부터 hand[4] 까지 모두 0으로 초기화
// 두번째 구문(GPA 배열)과 같은 원리의 초기화 방법
int things[] = {-1, 0, 1, 2};
// things array의 size가 자동적으로 원소의 개수에 맞춰서 4로 지정된다.
// 배열의 크기를 컴파일러가 결정하는 것이기 때문에 권장되지 않는 방법이다.
※ \(\texttt{arraySize}\)를 컴파일러가 결정하게 하는 것(예시 코드의 4번째 경우)은 권장되지 않는 방법이다.
- 프로그래머가 의도한 배열의 크기와 실제로 지정된 크기에 차이가 생길 수 있기 때문인데, 이는 logic error를 야기할 수도 있다.
- 예외적으로 문자열 구현을 위한 \(\texttt{char}\) type Array에는 대괄호 안 인덱스 값을 의도적으로 비워두는것이 오히려 더 안전한 방법이 될 수 있다.
* 리스트 초기화
- narrowing 을 자동적으로 방지해주는 초기화 방법
int algeb[5] {-3, -11, 0, 12, 134};
char tlifs[4] {'h', 'i', 112, '\0'};
// 자동적으로 방지되는 narrowing 의 예시
short point[3] {131070, 13, -987674}; // Not allowed
char slifs[4] {'h', 'i', 1122011, '\0'}; // Not allowed
- 배열의 크기는 (해당 배열 원소들의 데이터 형이 차지하는 메모리 크기) * (원소의 개수)이다.
// Ex. 배열의 크기 계산 예시
short things[] = {1, 3, 5, 7, 9};
cout << sizeof(things) / sizeof(short);
// things의 메모리 사이즈 = short의 메모리 사이즈 * 원소의 개수
// short가 2 Byte인 시스템일 경우, 10 이 출력될 것이다. (2 * 5)
배열의 주소
- 배열의 이름은 그 자체로 배열의 첫 번째 원소의 주소를 저장하는 변수와 같다.
- \(\&mathrm{arrayName}\)은 전체 배열의 주소를 반환하는데, 단순한 배열이름 (\(\mathrm{arrayName}\))과는 아래 코드와 같은 차이가 있다.
int arrayName[10]; // 40Byte만큼의 배열 생성 (각 원소는 4Byte)
arrayName + 1; // arrayName[0]에서 4만큼의 주솟값을 더하는 연산
&arrayName + 1; // arrayName[0]에서 40만큼의 주솟값을 더하는 연산
함수의 매개변수로써의 배열
// 기본형
typeName functionName (typeName arr[]);
typeName functionName (typeName* arr);
// 식별자 생략형
typeName functionName (typeName []);
typeName functionName (typeName*);
// 실 사용 예시
const double* f1 (int Arr[]);
int* f2 (char* ptr);
- 위 코드의 표기법 중 어느 것을 사용해도 결과는 동일하다.
- \(\texttt{sizeof}\) 연산자를 사용하는 경우, 배열을 지시하는 포인터일지라도 포인터는 포인터이기에 \(\texttt{sizeof}\) 연산자를 사용할 경우 배열의 크기가 아닌 포인터 그 자체의 크기가 리턴된다.
(배열이름에 \(\texttt{sizeof}\) 연산자를 사용할 경우, 정상적으로 배열 전체 크기가 리턴된다.)
- 일반 데이터형과 마찬가지로, 매개변수로 들어오는 배열이 수정되는 것을 원치 않을때는 \(\texttt{const}\) 키워드를 이용할 수 있다.
int Func (const double* arr); // 매개변수로 들어오는 함수를 "Func()" 함수 내에선 수정할 수 없다.
int Func (const double arr[]); // 위 구문과 동일한 구문이다.
- 정확히는 배열 \(\texttt{arr}\)이 상수 데이터를 지시하고 있다는 뜻인데, 원본 배열의 원소가 반드시 상수이어야 한다는 의미는 아니다.
- \(\texttt{const}\) 키워드가 사용된 함수 내에서 배열의 수정을 시도할 경우 컴파일 오류가 발생한다.
* 매개변수로써의 다차원 배열
int sum (int arr[][4], int size);
- 위 코드에서처럼, 함수 원형에서 행/열 중 크기가 지정된 것은 매개변수로 전달될 때 생략할 수 있다.
함수에 배열의 정보를 전달하는 2가지 방법
- 배열의 범위를 전달하는 것은 어떤 원소들을 처리해야 하는지를 함수에게 알려주는 것이다.
1. 배열이름(배열의 시작지점, 포인터), 배열크기를 매개변수로 전달하는 방법 (\(Typical \; Method\))
- 배열의 이름은 배열의 시작지점의 주소를 저장하고 있으므로 시작지점과 배열 전체의 크기를 알면,
호출한 함수가 배열의 범위를 알아낼 수 있다.
- 또한 배열이름은 배열 내 원소들의 데이터형에 대한 정보도 제공한다.
2. 배열의 시작 지점(포인터), 배열의 끝 지점(포인터)를 매개변수로 전달하는 방법 (\(STL \; Method\))
- 배열의 시작 지점과 끝 지점을 알면, 호출한 함수가 배열의 범위를 알아낼 수 있다.
- 주로 STL에 정의된 함수들 중 배열을 다루는 함수에서 이 방법이 많이 쓰인다.
- STL에서는 "One past the end (끝 바로 다음)" 기법을 이용하는데, 이는 배열의 끝을 인식하기 위한 배개변수로 배열의 마지막 원소 바로 다음을 지시하는 포인터를 이용하는 기법을 의미한다. (배열의 마지막 원소가 아님!)