본문 바로가기
Language

[Programming Language] 3. 자료형 (3)

by 삼준 2023. 7. 14.
반응형

직전글

2023.07.12 - [Language] - [Programming Language] 3. 자료형 (2)

● 레코드

개개의 원소들이 이름으로 식별되고 그 구조의 시작 부분으로부터 오프셋을 통해 접근되는 데이터 원소들의 집단체.

대부분의 언어에서 필드(= 레코드의 원소) 참조를 위해 도트 표기법을 사용함. (레코드이름.필드이름)

- C에서 레코드

struct(구조체)로 지원.

- Python에서 레코드

딕셔너리 or 해시로 구현 가능, 레코드가 배열의 원소가 될 수 있음.

- Java에서 레코드

클래스로 정의 가능, 클래스 멤버는 레코드 필드로서 역할 수행.

C에서 레코드 예시

 

● 튜플

레코드와 유사한 데이터 타입이지만, 원소들이 명명되지 않음.

- Python에서 튜플

변경불가 튜플 타입을 제공함. 변경해야 하는 경우 list() 함수를 이용해 배열로 변환 가능함.

매개변수로 전달되는 배열의 내용을 바꾸지 못하도록 하기 위해 사용됨.

myTuple = (3, 5.8, ‘apple’)  # 여기서 myTuple[1]을 한다면 두 번째 원소(5.8)를 참조하게 됨.

 

● 리스트

함수형 언어인 LISP에서 처음 사용되었고, 최근 일부 명령형 언어에 도입됨.

힙-동적 배열에서 설명한 Java의 ArrayList도 실제로 리스트로 볼 수 있음.

- Python에서 리스트

Python에서 리스트 테이터 타입은 배열로서도 역할을 수행함.
다른 언어들과 달리, Python에서의 List는 변경 가능함. (문자열, 튜플은 변경 불가)
리스트는 임의의 데이터 값이나 객체를 포함할 수 있음.

myList = [3, 5.8, "grape"]
x = myList[1]  # x == 5.8
del myList[1]  # myList == [3, "grape"]

Python은 리스트 포괄(List Comprehension)이라 불리는 배열을 생성하기 위해 집합 표기법에 기반한 메커니즘을 포함함.

 

ㄴ Python 리스트 포괄 구문
[식 for 반복_변수 in 배열 if 조건]

[x * x for x in range(12) if x % 3 == 0]  # 0~11에 있는 수 중 3으로 나누어 떨어지는 수들에 대해 제곱을 한 결과의 배열
# == [0, 9, 36, 81]

 

● 공용체

변수가 프로그램 실행 중, 다른 시기에 다른 타입의 값을 저장할 수 있는 타입.

- C, C++

타입 검사에 대한 언어 지원이 없는 공용체 구조를 제공함.

union flexType {
	int intEl;
	float floatEl;
};
union flexType el1;
float x;
...
el1.intEl = 27;
x = el1.floatEl;

el1은 4바이트 크기의 메모리를 가지고 있음.
마지막 배정문은 타입 검사되지 않음. 시스템이 el1의 현재 값의 현재 타입을 결정할 수 없기 때문. (정수를 저장하고 있는지 실수를 저장하고 있는지 모름)
따라서, 시스템은 27의 비트 표현을 float 변수 x에 배정함. (값이 올바르게 배정되었는지 여부는 관심 없음)

 

● 포인터 타입, 참조 타입

변수가 메모리 주소와 특수 값 (nil)으로 구성되는 값들의 범위를 갖는 타입.

nil은 유효한 주소가 아니며, 포인터가 메모리 셀을 참조하는데 현재 사용될 수 없음을 나타내는 데 사용함. (null 값)

- 포인터의 두 가지 사용 용도

1. 간접 주소지정 형태로 사용됨.
2. 동적으로 할당되는 힙(heap)이라 불리는 기억공간 영역의 한 위치를 접근하는 데 사용됨.

- 힙-동적 변수

힙으로부터 동적으로 할당되는 변수. 이 변수들은 흔히 이들과 연관된 식별자를 갖지 않음. (== 무명 변수(Anonymous Variable))
따라서, 포인터나 참조-타입 변수 (Reference Type Variable)들에 의해서만 참조될 수 있음

- 포인터 연산

포인터 타입을 제공하는 언어가 제공하는 두 가지 기본적인 포인터 연산.

1. 배정

포인터 변수의 값을 어떤 주소로 설정하는 것

2. 역참조(Dereferencing)

포인터 변수에 바인딩된 메모리 셀이 가리키는 메모리 셀에 포함된 값을 참조하는 것

ㄴ C언어 역참조

// ptr이 값으로 1000을 갖는 포인터 변수이고, 주소가 1000인 메모리 셀이 값 777을 포함하면
j = *ptr; // j에는 777이 배정됨

// 포인터가 레코드(구조체)를 가리킬 때
(*p).age  // '*'로 역참조 후, '.'으로 필드 참조
p->age    // '->'로 역참조와 필드참조를 한꺼번에 처리

C언어에서 힙 메모리 관리를 위해 명시적인 할당 연산을 수행함. (malloc() 라이브러리 함수)

객체지향 언어에서는 흔히 new 연산자를 사용함.

 

- 포인터의 문제

포인터 문제점 두 가지 그림으로 이해하기

1. 허상 포인터(Dangling Pointer)

이미 회수된 힙-동적 변수의 주소를 포함하는 포인터. 허상 참조(Dangling Reference)라고도 함.

 

ㄴ 위험한 이유

  • 가리키고 있는 기억장소 위치가 어떤 새로운 힙-동적 변수에 다시 할당되었을 경우, 새로운 변수의 타입과
    이전 변수의 타입이 일치하지 않을 수 있음.
  • 동일한 타입이라고 하더라도, 새로 할당되는 값은 이전 포인터의 역참조 된 값과 무관할 뿐만 아니라, 허
    상 포인터를 이용하여 값을 변경하면, 새로 할당된 값이 메모리 상에서 사라질 수 있음.
  • 허상 포인터가 가리키고 있는 메모리 위치를 기억공간 관리 시스템에서 임시로 사용하는 경우 (ex. 가용 기억공
    간 블록들의 체인에 대한 포인터로 사용하고 있는 경우), 기억공간 관리 시스템의 동작을 훼손할 수 있음.

ㄴ C언어 허상 포인터 생성 예시

int *arrayPtr1;
int *arrayPtr2 = (int*) malloc (sizeof (int)*100 );
arrayPtr1 = arrayPtr2;
free(arrayPtr2);
// 이제 arrayPtr1은 허상 포인터임
// arrayPtr2 또한 허상 포인터임

그림 설명

2. 분실된 힙-동적 변수

사용자 프로그램에서 더 이상 접근될 수 없는 할당된 힙-동적 변수. 흔히 쓰레기 (garbage)라고 부름.
이 변수들은 원래의 목적에 따라 사용될 수도 없고, 또한 새로운 용도로 프로그램에게 다시 할당될 수도 없음.

 

ㄴ C언어 쓰레기 생성 예시

int *p1;
int p2;
p1 = (int *)malloc(sizeof(int)*100);
p1 = &p2;
// 이제 malloc()으로 할당받은 힙-동적 변수는 쓰레기가 됨

그림 설명

malloc()으로 할당된 힙-동적 변수는 접근할 수 없음. 즉, 분실되었음. 메모리 누수(Memory Leakage)라고도 부름.

 

- C와 C++의 포인터

간접 주소지정방식으로 사용될 수 있으며, 임의의 변수를 가리킬 수 있음. 즉, 매우 유연함. 그러나, 허상 포인터와 분실된 동적 변수의 문제에 대해 어떤 해결책도 제공하지 않으며, 포인터 산술 연산이 가능하도록 허용하므로 매우 조심스럽게 사용되어야 함.

C와 C++의 포인터는 함수를 가리킬 수 있음. (함수 포인터)

void * 타입의 포괄형 포인터(generic pointer)도 지원함. 그러나, 포괄형 포인터의 역참조는 불허함 (역참조 할 크기를 모르기 때문)

- 참조 타입(Reference Type)

포인터와 유사하나, 한 가지 중요하고 근본적인 차이점을 가짐.
포인터는 메모리의 주소를 참조하는 것에 반해서 참조 변수는 메모리의 객체나 값을 참조함.
그 결과, 주소에 대해서 산술 연산을 하는 것은 자연스럽지만, 참조에 대해서 산술 연산을 하는 것은
무의미함.

참조 변수 그림 설명

Java 클래스 인스턴스들은 묵시적으로(명시적인 기억공간 회수 연산자가 없음) 회수되므로 허상 참조가 존재할 수 없음.
Python의 모든 변수는 참조 변수이며 항상 묵시적으로 역참조 됨. (즉, C언어의 ‘*’와 같은 연산자 필요하지 않음)

 

- 포인터와 참조 타입의 구현

대부분의 언어에서, 포인터는 힙 기억공간 관리에 사용됨.

 

1. 표현

대부분의 대형 컴퓨터에서, 포인터와 참조는 메모리 셀에 저장되어 있는 한 개의 값임.
그러나, 인텔 마이크로프로세서에 기반하는 초창기 마이크로컴퓨터에서 주소는 두 개의 부분으로 구성되었음.

(1) 세그먼트, (2) 오프셋
따라서, 이러한 시스템에서 포인터와 참조는 2개의 16비트 셀로 구현됨. (총 32 비트)

세그먼트와 오프셋 그림 설명

 

2. 허상 포인터 문제의 해결책

현재까지 제안된 것 중, 가장 좋은 해결책으로 “묵시적 회수” 방법이 존재함.
프로그래머를 거치지 않고, 힙-동적 변수를 회수하는 방법으로, 힙-동적 변수가 더 이상 유용하지 않을 때 실행-시간 시스템이 묵시적으로 회수함.

Java, LISP 등에서 사용함. (Java의 가비지 컬렉터는 이후 추가됨)

 

3. 힙 메모리 관리

가장 전통적인 두 가지 기법


(1) 참조 계수기 방법 (그때그때 회수하기)
회수가 점차적으로 이루어지며, 접근될 수 없는 셀들이 생성될 때 수행됨 → 조기 접근 방법 (Eager Approach)

 

ㄴ 동작 개요
모든 셀에 대해서 현재 자신을 가리키고 있는 포인터들의 개수를 저장하는 계수기를 유지함.
포인터가 셀로부터 분리될 때, 참조 계수기 감소 연산(Decrement Operation) 수행함.
참조 계수기의 값이 0이면, 셀이 쓰레기이므로, 가용 공간 리스트에 반환 가능해짐.


ㄴ 문제점
기억공간 셀들의 크기가 상대적으로 작다면, 계수기에 의해 요구되는 공간 부담이 존재함. (필요한 기억공간의 크기가 1바이트라면, 카운터는 4바이트여야 하므로 배보다 배꼽이 더 큰 상황이 됨)

계수기의 값을 유지/관리하기 위한 실행 시간이 필요함.
셀들의 모임이 순환적으로 연결되어 있다면 문제가 복잡해짐. (순환 리스트에 속한 셀은 적어도 1의 참조 계수기를 갖기 때문) → 해결책들이 제시되었으나, 배우지 않아 SKIP

ㄴ 장점

점차적으로 실행되기 때문에, 응용 프로그램 실행에 상당한 지연을 초래하지 않음.


(2) 표시-수집 방법 (한 번에 회수하기)
가용 기억공간이 부족할 때만 회수가 실행됨 → 지연 접근 방법 (Lazy Approach)

 

ㄴ 동작 개요
실행-시간 시스템은 요구될 때 기억공간 셀들을 할당하고, 필요할 때 포인터들을 셀들로부터 분리시킴.
이러한 과정은 기억공간 회수에 상관하지 않으면서 (쓰레기가 누적되는 것을 허용하면서) 가용 기억공간의 모들 셀들이 할당될 때까지 이루어짐.
이 시점에서, 표시-수집 프로세스는 힙 공간에서 떠다니고 있는 모든 쓰레기를 모으기 시작함.
쓰레기 수집 과정이 용이하도록, 힙 공간의 모든 셀은 그 수집 알고리즘에서 사용되는 여분의 지시자 (Indicator)인 비트나 필드를 가짐.

 

ㄴ 문제점
이 방법이 너무 드물게 수행됨. (그것도 프로그램이 거의 모든 힙 기억공간을 사용하였을 경우에만)
한 번 동작하기 시작하면, 상당히 많은 시간을 소모함.

 

ㄴ 결과

점차적 표시-수집 (Incremental Mark-sweep) 절차가 제안됨.
점차적 표시-수집 방법은 메모리가 소진되기 훨씬 이전에 더 빈번하게 수행되어 응용 프로그램의 실행 지연을 단축시킴.

반응형

댓글