본문 바로가기

자료구조

배열, 구조체

배열

 

1. 배열 인덱스

 

배열(Array)이란 동일한 데이터 타입의 변수 여러 개를 하나로 묶어서 관리하기 위한 것으로, 인덱스를 사용하여 필요한 요소(Element)가 있는 곳을 단번에 찾아 갈 수 있다는 것이 가장 큰 특징이다. (직접 접근, Direct Access) 원하는 요소를 찾기 위해서는 해당 요소가 있는 주소를 알아내야 하는데, 해당 인덱스로부터 주소를 찾는 작업은 컴파일러가 계산하며 이는 다음과 같다.

i번째 요소의 시작 요소 = &A[0] + (i - 1) x sizeof(Element Type);

 

이러한 계산이 가능한 근본적인 이유는 배열요소가 메모리 내에 서로 붙어 있기 때문이다. 배열 A의 시작 주소가 200번지라면 A[0]는 200번지에, A[1]은 204번지에서 시작한다.

 

때에 따라서는 2차원 배열을 사용하기도 한다. 이는 행과 열로 구성된 도표를 저장하기에 유리한 형태의 배열이다. 2차원(평면)이란 말은 행렬의 모습이 그렇다는 것이지 실제로 메모리 내의 배열은 여전히 1차원(선형)으로 진행한다. 포트란에서 열 우선 순서(Column-Major Order)를 사용한다면 C에서는 행 우선 순서(Row-Major Order)를 사용한다. 앞에 나오는게 행의 개수, 뒤에 나오는게 열의 개수가 된다는 뜻이다.

 

C/C++에서 문자열(String, Character String)은 배열에 저장된다. 마지막에 '\0' 이라는 심볼을 사용해 문자열의 끝을 나타낸다.

 

배열 크기는 선언 시에 고정되며 프로그램 실행 도중 크기를 줄이거나 늘릴 수 없다. 너무 작은 공간을 할당하여 실수로 허용된 공간 밖에 있는 주소에 접근해버리면 인덱스 범위 초과(Array Index out of Range)라는 오류에 직면하게 되고, 반대로 너무 큰 크기로 잡게 된다면 대부분의 경우 배열의 앞부분만 사용되고 뒷부분은 비게 되어 메모리 공간이 낭비되게 된다.

 

 

 

2. 배열과 포인터

 

배열의 정체는 포인터다. 예를 들어 int A[3] = { 20, 50, 80}; 이라고 선언하면 컴파일러에서는 이 명령을 다음과 같은 어셈블리 명령어 (Assembly Language Command)로 번역한다.

 

  1. 연속된 4바이트 공간 세 개를 확보하라.
  2. 각각의 공간에 20, 50, 80 값을 넣어라.
  3. 정수 포인터 A를 만들어라.
  4. 포인터 A의 값을 배열 첫 요소의 시작 주소 값으로 초기화하라.

결과적으로 포인터 변수인 배열 A = &A[0]이 되어버리므로 배열의 첫 요소는 A[0] 또는 *A라 표현할 수 있다.

 

배열명은 포인터지만 이 포인터는 상수 포인터다. 따라서 프로그램 실행 중에 그 내용인 주소 값을 변경할 수 없다. 실행을 위해 프로그램을 메인 메모리에 올릴 때(Loading Time) 배열 위치가 고정되기 때문이다. 

 

 

 

3. 동적 배열

 

프로그램 실행 중(Run Time)에 새로운 배열을 만드는 것을 말한다. C 언어의 malloc이나 C++ 언어의 new에 의해서 새로운 메모리 공간을 만들어내는 것과 동일하다. 

void Function1 (int Size) {
	int MyArray[Size];
} // 정적 배열 선언

void Function2 (int Size) {
	int *MyArrayPtr = new int[Size];
	delete[] MyArrayPtr;
} // 동적 배열 선언

 

Function1은 문법 오류(Syntax Error)를 일으킨다. 지역 변수인 정적 배열을 선언하는 데 있어서 배열 크기는 컴파일 시에 고정되어야 한다. 즉 실행 시간(Run Time)에 넘겨질 파라미터인 Size 값으로 배열의 크기를 잡을 수는 없기 때문이다. 정적 배열을 비롯한 모든 지역 변수는 스택 메모리(Stack Memory)라는 메모리 공간에 올려지며, 이 스택 메모리는 항상 프로그램 실행 전에 그 크기가 정해져야 한다.

 

반면 Function2의 동적 배열은 실행시간에 new에 의해 새롭게 만들어 질 수 있다. new에 의해 만들어지는 변수는 힙 메모리(Heap Memory)라는 메모리 공간에 올려지며, 이 힙 메모리는 프로그램 실행 중 필요에 따라 그 크기를 정할 수 있다.

 

 


 

 

구조체

데이터를 조직화하는 데 있어 필요한 기본 단위는 필드(Field)다. 각각의 필드가 모인 것이 레코드(Record)이고, 레코드 여러 개가 모여 하나의 파일(File)을 이룬다. (bit - byte - Field - Record - File - DB - BigData / Data warehouse)

 

 

 

구조체(Structure)란 레코드를 의미한다. 즉 필드 여러 개를 조합해서 하나의 단위로 묶은 자료구조를 말한다. 배열, 구조체 등은 데이터 여러 개를 모아서 그룹화한 것으로, 이러한 데이터 타입을 군집 데이터 타입(Aggregate Data Type)이라 한다. 배열이 동일한 타입의 데이터를 묶은 데 반해서, 구조체는 일반적으로 서로 다른 타입의 데이터를 묶은 것이다.

 

 

구조체 선언

 

typedef struct car 
{
	char Title[12];
 	int Year;
    int Price;
} carType;

 

typedef A B;는 기존의 A 타입을 내가 B라는 이름으로 명명한다는 것이다. 

 

 


 

 

활성화 레코드

프로그램을 실행할 때 메모리는 두 개의 섹션으로 나뉜다. 기계어 코드(Machine Code)가 들어있는 코드 섹션(Code Section)과 데이터가 들어있는 데이터 섹션(Data Section)이다. 코드 섹션은 프로그램 실행을 위해 읽을 수만 있는 부분이다. 데이터 섹션은 다시 전역 메모리(Global Memory)스택 메모리(Stack Memory), 그리고 힙 메모리(Heap Memory)로 나뉜다. 프로그램 내에서 데이터가 어떻게 선언되어 있느냐에 따라서 컴파일러는 이들 세 가지 중의 한 가지 메모리를 할당하게 된다.

 

모든 지역 함수(Local Function)의 바깥에 선언된 변수(Global Variable)에 할당되는 것이 전역 메모리다. 전역 변수는 메인 함수를 부르기 전에 이미 할당되어 프로그램이 종료되기 전까지는 그대로 남아 있게 된다.

함수 내에 선언된 지역 변수는 그 함수가 호출될 때 비로소 그 공간이 확보된다. 이를 위해 지역 변수는 필요한 공간의 크기를 컴파일 시에 확정해야 한다. 지역 변수를 위한 공간은 스택 메모리에 할당되며 이 스택 메모리는 해당 함수 실행이 끝나면 자동으로 해제되어 더 이상 사용할 수 없게 된다. 지역 변수가 함수 호출 시 생성되며 함수에서 빠져 나올 때 소멸된다는 것은 스택 메모리가 이러한 방식으로 운영되기 때문이다.

프로그램이 시작될 때 일정한 양의 힙 메모리가 프로그램에 할당된다. 프로그램 실행 중 필요에 의해 동적으로 할당되는 메모리가 힙 메모리다. 힙 메모리의 크기는 운영체제가 정한 최대 가상 메모리(Virtual Memory) 만큼이다. 

 

 

 

가장 낮은 번지에 프로그램이 들어간다. 물론 이 프로그램은 컴퓨터가 바로 실행할 수 있는 이진수 형태의 기계어 코드다. 그 위에 포그램에서 선언한 전역 변수들이 들어간다. 힙 메모리는 프로그램 실행 중에 동적으로 할당되며 함수 호출이 계속됨에 따라 점차 아래(낮은 번지)로 자라게 된다. 스택 메모리 역시 함수 호출 시에 정적으로 할당되며 점차 위(높은 번지)로 자란다. 따라서 아래로 자라는 힙 메모리와 위로 자라는 스택 메모리 사이에 있는 가용 메모리는 점차 줄어들게 된다.

 

스택 메모리는 활성화 레코드와 직결되어 있다. 재귀호출을 비롯한 모든 함수 호출에는 반드시 활성화 레코드(Activation Record)가 수반된다. 여기서 '활성화'라는 말은 피호출 함수가 활성화(가동) 되는 데 필요하다는 의미에서 붙여진 이름이다. 

 

운영체제가 함수를 호출하는 순간 함수의 활성화 레코드가 생성된다. 활성화 레코드는 스택에 저장되는 것으로서 일명 스택 프레임(Stack Frame)이라고도 한다. 

 

리턴 값 (Return Value) : 피호출 함수가 호출 함수에게 되돌려 주는 값.

값 파라미터 (Value Parameter) : 호출 함수가 피호출 함수에게 넘기고자 하는 값.

귀환 주소 (Return Address) : 호출 함수가 명령문에서 함수 호출 후 수행이 끝난 다음 돌아갈 명령문의 번지수.

지역 변수 (Local Variables) : 함수 내에서 선언된 지역 변수를 저장하기 위한 곳.

 

함수 호출

  • 새로운 활성화 레코드 생성
  • 연속적인 함수 호출에 따라 활성화 레코드 스택이 생성
  • 함수 종료시 제일 위의 활성화 레코드가 사라짐으로써 직전의 상황을 복원

만약 실수로 무한 루프 내에서 함수 호출을 계속한다면 활성화 레코드가 무한정 쌓이게 되고 그 결과 운영체제가 할당한 메모리 공간을 초과함으로 인해 스택 오버 플로우 오류에 직면. 무한 루프 내 new int;를 계속했다면 이번에는 힙 오버 플로우에 직면. 동적 메모리는 힙 메모리에 할당되기 때문.

'자료구조' 카테고리의 다른 글

덱/데크 (Deque), 우선순위 큐 (Priority Queue)  (0) 2024.03.28
큐 (Queue)  (1) 2024.03.27
스택 (Stack)  (0) 2024.03.27
리스트  (1) 2024.03.26
추상 자료형 (ADT, Abstract Data Type)  (0) 2024.03.21