프로그래밍 언어 - Pointer and Refernece Types
2023-12-15. 12:20 : 글 계승.
🌑 프로그래밍 언어 - Pointer and Reference Types
💫 포인터 Pointer
@ U 기말고사 출제 : C 포인터를 사용하는데 있어서 발생할 수 있는 문제인 Dangling Pointer
와 Memory Leaking
에 대하여 설명하시오. 이에 대한 해결책도 같이 설명하시오.
- 포인터 변수는 메모리 주소를 가지는 타입이다.
특수값 NIL/NULL 주소를 가지면, 메모리 참조를 할 수 없음을 의미한다.
- 포인터 설계의 용도
- 간접 주소지정 방식을 지원하기 위해
- Scope룰에 의해 직접 접근이 안되는 대상에 간접적으로 접근하기 위해 사용됨
- 동적 기억공간(힙 heap)을 관리하는 방식을 제공하기 위해
- 간접 주소지정 방식을 지원하기 위해
- 힙-동적 변수
- 힙공간에 동적으로 할당되는 변수
- 식별자(이름)가 없음 -> 무명 변수 (힙이 Nameless 로 불리기도 함)
- 따라서 포인터를 이용하여 간접적으로 접근이 가능
- 포인터는 동적 자료구조의 작성력을 향상시킴
- Fortran 77에서는 동적 기억공간이 없음 그리고 이진 트리와 같은 동적 구조를 배열과 같은 이용하여 구현해야 하므로 작성력이 떨어짐
- 설계 고려 사항
- 포인터 변수의 영역과 존속기간은 무엇인가?
- 힙-동적 변수(포인터가 참조하는 값)의 존속기간은 무엇인가?
- 포인터는 가리킬 수 있는 값에는 제약이 있는가?
- 포인터가 동적 기억공간 관리, 간접 주소지정 또는 두 가지 모두를 위해 사용되는가?
- 언어가 포인터 타입, 참조 타입, 또는 두 가지 모두를 지원하는가?
- C/C++의 포인터
- 어셈블리에서 주소를 사용하는 방식으로 C/C++에서 주소를 사용 가능
- 유연성을 제공하나 허상 포인터(dangling pointer)나 메모리 누수(memory leaking)과 같은 문제에 어떤 해결책도 제시하지 않음 -> 매우 조심해서 사용해야 함
- 포인터 연산
- *: 역참조 연산
- &: 변수의 주소를 생성하기 위한 연산
- +: 포인터의 주소 덧셈 연산
- 배열의 요소 접근을 위해 사용
- 포인터 정의
- “타입 *“을 이용하여 포인터를 정의 함
- 변수의 주소 계산 및 역참조
- 변수의 주소를 계산할 경우 & 연산자를 사용
- 주소값에 대한 역참조를 수행할 경우 * 연산자를 사용
- 예) pointer_basic.c
- Scope이 다른 변수에 대한 간접 참조
- Scope룰에 의해 직접 접근이 안되는 대상에 간접적으로 접근 하기 위해 사용 됨
- 예) pointer_indirect.c
- 배열과 포인터
- C/C++에서 배열명은 주소값, 포인터는 변수
- C/C++에서 배열 자체를 함수의 인자로 넘길 수 없으며 포인터를 사용해야 함
- 주소값 연산을 통해 항목들을 접근하는 것이 가능
- 배열의 요소 접근 a[1]은 실제로 C 컴파일러에의해 *(a + 1)로 처리
- C Programming Language의 “5.3절 Pointers and Arrays” 참조
- 예) pointer_array_1.c, pointer_array_2.c, pointer_array_cpp.cpp
- 구조체와 포인터
- 구조체의 멤버를 접근할 경우 . 연산자를 사용
- 구조체를 포인터로 지정할 경우 (*ptr).member로 사용해야하며 이는 ptr->member로 사용 가능
- 예) pointer_struct.c, pointer_struct_cpp.cpp
- 구조체의 멤버를 접근할 경우 . 연산자를 사용
- 어셈블리에서 주소를 사용하는 방식으로 C/C++에서 주소를 사용 가능
- 포인터가 유발할 수 있는 문제
- Dangling pointer (Stray pointer, 허상 포인터)
- 이미 회수된 힙-동적 변수의 주소를 가지고 있는 포인터
- 문제점
- 포인터가 가리키는 메모리가 회수된 후 다시 할당 된 경우 이전 포인터를 이용한 값의 변경은 문제를 발생시킬 수 있음
- 예) dangling_pointer.c
- 해결책
- 포인터가 가리키는 메모리가 회수된 경우, 포인터를 NULL로 설정한다.
- Memory leaking (메모리 누수)
- 더 이상 접근할 수 없는 힙-동적 변수(쓰레기)가 발생하는 현상
- 쓰레기(Garbage): 변수들이 원래의 목적에에 유용하지 않고, 프로그램에서 새로운 용도로 다시 활용할 수 없는 메모리
- 예) memory_leaking.c
- 해결책
- 힙-동적 변수를 다 사용하고 나면 반드시 반납을 하라.
- C에서는 free함수 C++에서는 delete 연산
- 힙-동적 변수를 다 사용하고 나면 반드시 반납을 하라.
- 더 이상 접근할 수 없는 힙-동적 변수(쓰레기)가 발생하는 현상
- Dangling pointer (Stray pointer, 허상 포인터)
- 참조 타입
- 메모리의 객체나 값을 참조하는 용도
- 포인터는 메모리의 주소를 참조
- 포인터는 주소에 대한 산술 연산이 가능하나, 참조는 주소에 대한 산술 연산이 허용되지 않음
- C++에서의 참조 타입
- 묵시적으로 역참조되는 상수 포인터로 변수의 정의의에서 &를 사용
- C++의 참조 타입은 한번 초기화되면 다른 변수를 참조하도록 설정할 수 없음
- 예) reference_1.cpp
- 참조 타입을 이용하여 형식 매개변수의 전달에 활용 (컴파일러가 주소 전달)
- 포인터는 가독성이 떨어지고 안전하지 않은 연산을 유발할 수 있음
- 하지만 참조 타입 또한 원본 값을 변화시키는 부작용을 유발할 수 있음
- 예) reference_2.cpp
- Java에서의 참조 타입
- 안정성 향상을 위해 C/C++ 유형의 포인터를 제거
- Java에서는 객체를 참조할 수 있도록 사용하며 변수 임(상수가 아님)
- Java에서 객체들은 Managed-Heap에 할당되며 참조를 통해 접근됨
- 어떤 참조도 되지 않는 객체는 GC(Garbage Collector)에 의해 회수 -> 메모리 누수가 없음
- 예) StudentExam.java
- C#
- 포인터와 Java 스타일의 참조를 모두 포함, 하지만 포인터 사용은 권장치 않음
- C/C++과의 연동을 위하여 포함됨, 포인터 사용시 unsafe라는 지정자 필요
- 참조된 객체는 묵시적으로 회수되나 포인터를 이용하여 참조된 객체는 명시적 회수가 필요
- 포인터와 Java 스타일의 참조를 모두 포함, 하지만 포인터 사용은 권장치 않음
- Smalltalk, Python, Ruby, Lua
- 모든 변수는 참조 타입이며 모든 대상은 객체임
- 예) student_exam.py
- 묵시적으로 역참조되는 상수 포인터로 변수의 정의의에서 &를 사용
- 메모리의 객체나 값을 참조하는 용도
- 평가
- 포인터는 허상 포인터(Dangling Pointer)와 메모리 누수(Memory Leaking)과 같은 문제 내포
- 포인터 변수는 메모리의 참조 범위를 확대하는 경향이 있음
- “고급언어에서 포인터의 도입은 결코 회복할 수 없는 한 걸음의 후퇴였다” - Hoare
- 포인터는 디바이스 드라이버(장치 관리자)와 같은 시스템 프로그램 작성에는 필수적인 요소
- Java, C#, Python등의 참조 변수는 안정성과 유연성을 제공
- 결국 포인터와 참조 변수는 성능(혹은 자유)와 안전성의 Trade-off
💫 과제 1
교재와 기타 자료들을 참고하여, Pointer와 Reference Type들을 정리하고 이해하라.
교재 ‘Concepts of Programming Languages’, 6.11 Pointer와 Reference Type
💫 Pointer
메모리 주소, 특수 값, nil
(null) 값을 가질 수 있는 변수다.
nil은 유효한 주소가 아니며, 현재 해당 포인터가 메모리 셀을 참조하는 데에 쓸 수 없음을 나타내는 데 쓰인다.
포인터는 두 가지 용도로 설계되었다.
- 어셈블리에서 주로 사용되는 간접 주소 지정
- 동적 저장공간 관리
포인터를 사용하여, 힙 - Heap
에 동적으로 할당된 저장공간 위치에 접근할 수 있다.
힙으로부터 동적으로 할당되는 변수를 힙 동적 변수 - Heap-Dynamic Variables
라고 한다.
대부분 식별자를 따로 가지고 있지 않고, 포인터 또는 참조 변수를 통해서만 참조할 수 있습니다.
이름 없는 변수를 익명 변수 - Anonymous Variables
라고 한다.
2번 용도에서 가장 중요한 설계 문제가 생긴다. 포인터는 형식 연산자(* C와 C++, access Ada)를 사용하여 정의되지만, 배열이나 레코드와는 달리 구조화된 형식은 아니다. 또한 데이터를 저장하는 데 사용되는 것이 아니라, 다른 변수를 참조하는 데 사용되기 때문에 스칼라 변수와는 다르다.
이 두 범주의 변수를 각각 참조 유형 - ReferenceTypes
과 값 유형 - ValueTypes
이라고 한다.
포인터를 사용하는 두 유형 모두 프로그래밍 언어의 작성력에 도움을 준다.
i,e,
포인터가 없는 Fortran 77 같은 언어로 이진 트리처럼 동적 구조를 구현해야 한다면, 프로그래머가 가용한 트리 노드 풀을 제공하고 유지해야 하며, 이는 병렬 어레이로 구현될 가능성이 높다. 또한 Fortran 77에는 동적 저장 공간이 없기 때문에 프로그래머가 필요한 최대 노드 수를 추측해야 할 것이다. 이는 분명 어색하고, 오류가 발생하기 쉬운 처리 방법이다.
참조 변수는 포인터와 밀접한 관련이 있다.
💫 설계 문제 - Design Issue
- 포인터 변수의 범위와 수명은 얼마인가?
- 동적 변수(포인터가 참조하는 값)의 수명은 얼마인가?
- 포인터가 가리키는 값의 유형에 제한이 있나?
- 포인터의 용도가 동적 스토리지 관리, 간접 주소 지정 또는 둘 다 인가?
- 언어는 포인터 유형, 참조 유형 또는 둘 다를 지원해야 하나?
💫 포인터 연산들
포인터 타입을 제공하는 언어들은 대개 두 가지 기본적인 포인터 연산을 포함한다.
- 할당 - Assignment
- 역참조 - Dereferencing
식에서 포인터 변수의 발생은 두 가지 방법으로 해석할 수 있다.
- 일반적인 참조 : 포인터 바인딩된 메모리 셀 자체의 내용 참조
- 수식에서 비포인터 변수는 정확히 이런 식으로 해석되지만, 그 경우에는 값이 주소가 아닐 가능성이 높다.
- 간접적인 참조 (
역참조
) : 포인터가 바인딩된 메모리 셀이 가리키는 메모리 셀 내의 값을 참조하는 것
🫧 할당 - Assingment
포인터 변수의 값을 어떤 유용한 주소로 설정한다.
포인터 변수를 오직 동적 저장공간 관리에만 사용한다면, 할당 메커니즘은 연산자에 의해서든 내장 함수에 의해서든 포인터 변수를 초기화하는 역할을 한다.
포인터 변수를 오직 간접 주소 지정을 위해서 사용한다면, 변수의 주소를 가져오는 명시적 연산자나 내장 함수이 있어야 포인터 변수에 할당할 수 있다.
힙의 관리를 위한 포인터를 제공하는 언어는 명시적인 할당 연산을 포함해야 한다.
C의 malloc과 같은 함수로 지정되기도 한다.
객체 지향 언어에서는 힙 객체의 할당을 new 연산자로 지정하는 경우가 많다.
암묵적인 할당 해제를 제공하지 않는 C++ delete를 할당 해제 연산자로 사용한다.
🫧 역참조 - Dereference-ing
한 단계의 방향성을 통해 참조를 취하는 것. 명시적이거나 암묵적일 수 있다.
Fortran 95+에서 암시적이지만, 다른 많은 현대 언어에서는 명시적으로 지정된 경우에만 발생한다.
C++에서는 접두사 단항 연산자로 별표(*)와 함께 명시적으로 지정된다.
i.e.
ptr이 값 7080인 포인터 변수이고, 주소가 7080인 셀의 값이 206인 경우,
할당 ‘j = *ptr’은 j를 206으로 설정하는 것이다.
포인터가 레코드를 가리킬 때 이러한 레코드의 필드에 대한 참조의 문법은 언어마다 다르다.
in C/Cpp
- (*p.age), 포인터 변수 p가 age라는 필드를 가진 레코드를 가리킬 때
- p -> age, 연산자
->
는 레코드에 대한 포인터와 해당 레코드의 필드 사이에 사용될 때 역참조와 필드 참조를 결합
Ada, 이러한 포인터의 사용이 암묵적으로 역참조되기 때문에 p.age를 사용할 수 있다.
💫 포인터의 문제들
포인터 변수를 포함한 최초의 고수준 언어 ‘PL/I’, 여기서 포인터는 동적 변수와 다른 변수를 모두 참조하는 데 사용될 수 있었다.
PL/I의 포인터는 매우 유연했지만 여러 오류를 발생시킬 수 있었다.
후속 언어들의 포인터들에도 PL/I 포인터들의 문제 중 일부가 그대로 존재한다.
최근 언어들은 포인터들을 참조 형식으로 완전히 대체했는데, 이는 암묵적인 할당 해제와 함께 포인터들의 주요 문제를 최소화한다. 참조 형식은 실제로 연산이 제한된 포인터일 뿐이다.
🫧 Dangling Pointers
Danling Pointer, Dangling Reference
할당 해제된 동적 변수의 주소를 포함하는 포인터다.
문제가 되는 이유
- 가리키는 위치가 어떤 새로운 동적 변수에 재할당 됐을 수 있다.
- 새로운 변수가 이전 변수와 동일한 유형이 아니라면, 댕글링 포인터의 용도에 대한 유형 검사는 유효하지 않다.
- 새로운 동적 변수가 동일한 유형이더라도, 그것의 새로운 값은 이전 포인터의 폐기된 값과 관련이 없을 것이다.
- 댕글링 포인터가 동적 변수 값을 변경하는 데 사용된다면, 새로운 동적 변수의 값은 파괴될 것이다.
- 현재 위치가 스토리지 관리 시스템에 의해 일시적으로 사용되고 있을 수 있고, 아마도 사용 가능한 스토리지 블록들의 체인에서 포인터로 사용될 수 있으므로, 스토리지 관리자가 실패하는 원인이 될 수 있다.
Danling Pointer가 만들어지는 과정
- 새로운 동적 변수 생성, 포인터 p1이 해당 변수를 가리키도록 설정.
- 포인터 p2에 p1의 값이 할당.
- p1이 가리키는 동적 변수는 명시적으로 할당 해제되지만 (p1을 nil로 설정할 수도 있음), p2는 연산에 의해 변경되지 않음.
- 이제 p2는 Danling Pointer.
- 할당 해제 연산이 p1을 변경하지 않았다면 p1과 p2는 모두 Danling Pointer일 것.
- (물론 이것은 Ailiasing의 문제, p1과 p2는 Aliases.)
i.e.
1
2
3
4
5
6
int * arrayPtr1;
int * arrayPtr2 = new int[100];
arrayPtr1 = arrayPtr2;
delete [] arrayPtr2;
// Now, arrayPtr1 is dangling, because the heap storage
// to which it was pointing has been deallocated.
배열 Ptr1과 Ptr2 모두 달링 포인터가 되는데, 이는 C++ delete
연산자가 피연산자 포인터의 값에 아무런 영향을 주지 않기 때문이다.
C++에서는 null을 나타내는 0의 할당을 가진 delete
연산자를 포인터에 따라 이동하는 것이 일반적이며 안전합니다.
동적 변수의 명시적인 할당 해제가 댕글링 포인터의 원인이라는 점에 유의하십시오.
delete
` 연산자가 피연산자 포인터의 값에 아무런 영향을 미치지 않기 때문에 배열 Ptr1과 Ptr2 모두 댕글링 포인터가 된다.
가리치던 값이 할당 해제된 포인터에 null을 나타내는 0을 할당하는 의미의 ‘delete’ 연산자를 따르는 것이 일반적이며 안전하다.
동적 변수의 명시적인 할당 해제가 댕글링 포인터의 원인이라는 점에 유의.
🫧 Lost Heap-Dynamic Variables
Lost Heap-Dynamic Variables
할당된 동적 변수지만, 더 이상 사용자 프로그램에서 접근할 수 없는 변수를 말한다.
이러한 변수들은 원래 목적에 유용하지 않고 프로그램에서 새로운 용도로 재할당될 수 없기 가비지(garbage)라고도 불린다.
Lost Heap-Dynamic Variables가 만들어지는 과정
- 포인터 p1은 새로 생성된 동적 변수 A를 가리키도록 설정
- p1은 나중에 새로 생성된 다른 동적 변수B를 가리키도록 설정
- A는 이제 접근할 수 없음, 유실됨. (메모리 누수 - Memory Leak)
메모리 누수는 언어가 암시적 또는 명시적 할당 해제를 사용하는지에 관계없이 문제가 된다.
💫 언어 별 포인터 (+ 포인터 문제 해결 방법)
🫧 Pointers in Ada
포인터를 Access 타입이라고 부른다.
‘Dangling Pointer’ 문제는 이론적으로는 Ada의 설계에 의해 부분적으로 완화된다.
동적 변수가 포인터 타입의 Scope 끝에 암묵적으로 (선택에 따라) 할당 해제될 수 있으므로, 명시적인 할당 해제의 필요성이 크게 줄어든다.
그러나 Ada 컴파일러 중에 이런 형태의 가비지 컬렉션을 구현하는 경우는 거의 없기 때문에, 대부분 이론적으로만 이점이 있다.
동적 변수는 오직 한 종류의 변수만 접근할 수 있기 때문에, 해당 타입 선언의 Scope 끝에 도달하면 동적 변수를 가리키는 포인터를 남길 수 없다.
이러면 문제가 줄어든다.
명시적 할당 해제인 Unchecked_Deallocation도 있긴 하지만, Dangling-pointer를 발생시킬 수 있기에, 이름을 통해 잠재적인 문제를 경고하고 있다.
‘Lost Heap-Dynamic Variables’ 문제는 설계로 해결되지 않는다.
🫧 Pointers in C and C++
주소가 어셈블리 언어에서 사용되는 것과 같은 방식으로 포인터를 사용할 수 있는데,
이는 매우 유연하지만 주의해서 사용해야 함을 의미한다. 이러한 설계는 포인터 문제들에 대한 해결책을 제공하지 않기 때문이다.
그러나 C/C++에서 포인터 연산이 가능하다는 사실은 다른 프로그래밍 언어보다 포인터가 더 흥미롭게 만든다. (리스크/리턴)
C와 C++ 포인터는 할당된 위치에 관계없이 임의의 변수를 가리킬 수 있다.
실제로 포인터의 위험 중 하나인 변수가 존재하든 존재하지 않든 메모리의 어느 곳을 가리킬 수 있다.
두 코드는 의미가 같다.
1
2
3
4
5
6
7
int *ptr;
int count, init;
// ...
ptr = &init;
count = *ptr;
1
2
int count, init;
count = init;
일부 제한된 형식으로 Pointer 연산이 가능하다.
예를 들어, ptr이 일부 데이터 유형의 변수를 가리키는 것으로 선언된 포인터 변수라면 ‘ptr + index’는 합법적인 표현식입니다.
ptr에서 (ptr이 대상으로 하는 타입 크기 * index) 만큼 떨어진 메모리 셀을 가리킨다.
이러한 주소 연산의 주된 목적은 배열 조작이다.
(1차원 배열에 대해 ) C/C++에서 모든 배열은 첨자 범위의 하한으로 0을 사용하며, 첨자가 없는 배열 이름은 항상 첫 번째 요소의 주소를 나타냅니다.
1
2
3
4
int list [10];
int *ptr;
ptr = list;
- *(ptr + 1)는 list[1]
- *(ptr + index)는 list[index]
- ptr[index]는 list[index]
포인터 연산이 인덱싱 연산에서 사용되는 것과 같은 스케일링을 포함한다는 것이 분명하다.
배열에 대한 포인터들은 배열 이름인 것처럼 인덱싱될 수 있다.
포인터들은 매개변수 전달에도 사용된다.
C/C++는 어떤 형식의 값이든 가리킬 수 있는 일반적인 포인터, void* 포인터들을 이용한다.
C/C++의 포인터들은 함수들을 가리킬 수 있다.
매개변수를 통해 함수들을 다른 함수에 전달하는 데 사용된다.
💫 Reference Type
Reference Type은 포인터와 비슷하지만, 중요하고 기본적인 차이점이 있다.
포인터는 메모리 내의 주소를 의미하고,
참고형은 메모리 내의 객체나 값을 의미한다.
따라서,
주소에 대해서는 연산을 수행하는 것이 당연하지만
참고형에 대해서는 연산을 수행하는 것이 적절하지 않다.
C++는 함수 정의에서 공식 매개변수에 주로 사용되는 특수한 종류의 참조형이 있다.
C++ 참조형 변수는 항상 암묵적으로 디레퍼런스되는 상수 포인터입니다.
C++ 참조형 변수는 상수이므로 정의에서 어떤 변수의 주소로 초기화되어야 하며,
초기화 후에는 절대로 다른 변수를 참조하도록 설정할 수 없습니다.
암묵적 디레퍼런스는 참조 변수의 주소 값에 할당되지 않습니다.
참조형 변수는 앰퍼샌드(&)로 이름 앞에 붙여 정의됩니다. 예를 들어,
1
2
3
result = 0;
int &ref_ result = result;
ref_result = 100;
이 코드 세그먼트에서 result 및 ref_result는 별칭(동의어)이다.
함수 정의에서 형식 매개변수로 사용되는 경우 C++ 참조 유형은 호출자 함수와 호출된 함수 간의 양방향 통신을 제공한다.
C++ 매개변수는 값으로 전달되기 때문에 비포인터 프리미티브 매개변수 유형에서는 불가능하다.
포인터를 매개변수로 전달하면 동일한 양방향 통신이 수행되지만 포인터 형식 매개변수는 명시적인 역참조를 필요로 하므로 코드의 가독성과 안전성이 떨어진다.
참조 매개변수는 다른 매개변수와 마찬가지로 호출된 함수에서 정확히 참조된다.
호출 함수는 해당 형식 매개변수가 참조 유형인 매개변수가 특이한 것임을 지정할 필요가 없다.
컴파일러는 참조 매개변수에 값이 아닌 주소를 전달한다.
Java는 안전성을 높이기 위해 C++ 스타일의 포인터를 아예 제거했다.
Java 참조 변수는, C++ 참조 변수와 달리 상수가 아니라 서로 다른 클래스 인스턴스를 참조하도록 지정할 수 있다.
모든 자바 클래스 인스턴스는 참조 변수에 의해 참조된다.
1
2
String str1; // null
str1 = "This is a Java literal string.";
str1은 String 클래스 인스턴스 또는 개체에 대한 참조로 정의된다.
초깃값 null, 이후 str1이 String 개체인 “This is a Java literal string.”를 참조.
C#, Java 클래스 인스턴스는 암묵적으로 할당 해제되므로 (명시적 할당 해제 연산자가 없음) Java에는 dangling reference가 있을 수 없다.
C#은 참조와 포인터를 모두 가진다.
하지만 포인터를 사용하는 것은 강력히 권장되지 않는다. (포인터를 사용하려면 unsafe
수식어를 포함)
참조로 가리킨 객체는 암묵적으로 할당 해제되지만, 포인터로 가리킨 객체의 경우에는 그렇지 않는다.
C/C++ 코드와 상호 작용할 수 있도록 포함됐다.
개체 지향 언어인 Smalltalk, Python, Ruby, Lua의 모든 변수는 참조다.
이들은 항상 암묵적으로 참조 해제된다.
게다가 이 변수들의 직접적인 값은 접근할 수 없다.
💫 평가
Lost Heap-Dynamic Variables, Danling Pointer
Pointer vs goto
goto 문은 다음에 실행될 수 있는 문장의 범위를 넓힙니다.
포인터 변수는 변수가 참조할 수 있는 메모리 셀의 범위를 넓힌다.
반면 포인터는 어떤 종류의 프로그래밍 응용 프로그램에서 필수적이다.
예를 들어 포인터는 특정한 절대 주소에 접근해야 하는 장치 드라이버를 작성하는 데 필요하다.
자바와 C#의 참조는 위험 요소 없이 포인터의 유연성과 기능을 어느 정도 제공한다.
프로그래머들이 참조의 안전성을 더 중요시 하여 C와 C++ 포인터의 완전한 힘을 기꺼이 교환할지는 두고 봐야 할 것.
C# 프로그램이 포인터를 사용하는 정도가 이것의 한 척도가 될 것.
💫 Implementation of Pointer and Reference Types
대부분의 언어에서 포인터는 힙 관리에 사용된다.
Smalltalk와 Ruby의 변수뿐만 아니라 Java와 C# 참조도 마찬가지이므로 포인터와 참조를 따로 다룰 수 없다.
🫧 Representations of Pointers and References
대부분의 컴퓨터에서 포인터와 참조는 메모리 셀에 저장된 단일 값이다.
그러나 인텔 마이크로프로세서를 기반으로 하는 초기의 마이크로컴퓨터에서는 주소가 세그먼트와 오프셋 두 부분으로 구성된다.
따라서 이러한 시스템에서 포인터와 참조는 16비트 셀 쌍으로 구현되며, 주소의 두 부분 각각에 대해 하나씩 구현된다.
🫧 Solutions to the Dangling-Pointer Problem
Dangling-Pointer 문제에 대한 해결책은 여러 가지가 제안됐다.
물론, 최선의 해결책은 동적 변수들의 할당을 프로그래머의 손에서 없애는 것이다.
프로그램들이 동적 변수들의 할당을 명시적으로 해제할 수 없다면, Dangling-Pointer들은 없을 것입니다.
이를 위해서는 런타임 시스템이 동적 변수들이 더 이상 쓸모가 없을 때 암묵적으로 할당을 해제해야 한다.
LISP 시스템들은 항상 이렇게 해왔고, Java와 C# 모두 참조 변수들에 대해서 이 접근법을 사용한다.
C#의 포인터들에는 암묵적인 할당 해제가 포함되어 있지 않음을 기억.
Tombstones
모든 동적 변수가 동적 변수에 대한 포인터인 ‘Tombstone’이라고 불리는 특수 셀을 포함하는 Tombstones
.
실제 포인터 변수는 ‘Tombstone’에만 있고 동적 변수는 절대 가리키는 것이 아니다.
동적 변수가 할당 해제되면 ‘Tombstone’은 그대로 유지되지만 ‘0’으로 설정되어 동적 변수가 더 이상 존재하지 않음을 나타낸다.
이 접근법은 포인터가 할당 해제된 변수를 가리키는 것을 방지한다.
0이 아닌 ‘Tombstone’을 가리키는 포인터에 대한 모든 참조는 오류로 탐지될 수 있다.
‘Tombstone’은 시간적으로나 공간적으로나 비용이 많이 든다.
‘Tombstone’은 결코 할당이 해제되지 않기 때문에, 그 저장공간은 다시 확보되지 않는다.
힙 동적 변수에 접근할 때마다 한 단계의 방향 전환이 더 필요하고, 대부분의 컴퓨터에서 추가적인 기계 사이클이 필요하다.
널리 사용되는 언어들 중 ‘Tombstone’를 쓰는 언어가 없다.
→ 언어 설계자들 중 그 누구도 그 추가적인 안전성이 이러한 추가적인 비용의 가치가 있다고 생각하지 않음.
Locks-and-Keys Approach
UW-Pascal 구현에 사용되는 Locks-and-Keys
접근법.
이 컴파일러에서 포인터 값은 순서쌍(key, address)으로 표현되며, 여기서 키는 정수 값이다.
동적 변수는 변수의 저장소에 정수 잠금 값을 저장하는 헤더 셀을 더한 값으로 표현된다.
동적 변수가 할당되면 잠금 값이 생성되어 동적 변수의 잠금 셀과 ‘new’ 호출에 지정된 포인터의 키 셀에 모두 배치된다.
디레퍼런드 포인터에 대한 모든 접근에서, 포인터의 키 값을 동적 변수의 잠금 값과 비교한다.
일치하면 액세스는 합법이고, 일치하지 않으면 런타임 오류로 처리된다.
다른 포인터에 대한 포인터 값의 복사본은 키 값을 복사해야 한다.
따라서 어떤 수의 포인터라도 주어진 동적 변수를 참조할 수 있다.
동적 변수가 ‘dis-pose’로 할당이 해제되면 해당 포인터의 잠금 값은 잘못된 잠금 값으로 지워진다.
그러면 ‘dis-pose’에 지정된 포인터가 아닌 다른 포인터가 디레퍼런드되면 주소 값은 그대로 유지되지만, 키 값은 더 이상 잠금과 일치하지 않으므로 액세스가 허용되지 않는다.
🫧 Heap Management
힙 관리는 매우 복잡한 런타임 프로세스가 될 수 있다.
글에서는 할당 해제의 경우 암묵적인 접근 방식만 논의.
프로세스 및 관련 문제에 대한 철저한 분석은 구현 문제만큼, 언어 설계 문제가 아니다.
Single-Size Cells
가장 간단한 상황은 모든 할당과 할당 해제가 단일 크기의 셀인 경우입니다. 모든 셀에 이미 포인터가 있을 때 더욱 단순화됩니다. 동적 스토리지 할당의 문제가 대규모로 처음 접했던 LISP의 많은 구현들의 시나리오입니다. 모든 LISP 프로그램들과 대부분의 LISP 데이터는 링크된 목록의 셀들로 구성됩니다.
단일 크기의 할당 힙에서는 사용 가능한 모든 셀들이 셀 안의 포인터들을 사용하여 서로 연결되어 사용 가능한 공간의 목록을 만든다. 할당은 필요할 때 필요한 수의 셀을 이 목록에서 가져오는 간단한 문제이다. 할당 해제는 훨씬 더 복잡한 과정이다. 힙-동적 변수는 하나 이상의 포인터로 가리킬 수 있으므로, 언제 그 변수가 프로그램에 더 이상 쓸모가 없는지 판단하기가 어렵다. 단순히 하나의 포인터가 셀에서 분리되었다고 해서 그것이 쓰레기가 되는 것은 아니며, 셀을 가리키는 다른 포인터들도 여러 개 존재할 수 있다.
LISP에서는 프로그램에서 가장 빈번한 작업 중 몇 가지는 더 이상 프로그램에 접근할 수 없으므로 할당 해제(사용 가능한 공간 목록에 다시 올려놓음)해야 하는 셀 모음을 만듭니다. LISP의 기본 설계 목표 중 하나는 사용되지 않은 셀의 회수가 프로그래머의 과제가 아니라 런타임 시스템의 과제가 되도록 하는 것이었습니다. 이 목표는 LISP 시행자들에게 기본 설계 질문을 남겼습니다: 언제 할당 해제를 수행해야 하는가?
쓰레기 수거에는 여러 가지 다른 접근 방식이 있습니다. 가장 일반적인 두 가지 전통적인 기술은 어떤 면에서는 반대의 과정입니다. 이것들은 매립이 증분이고 접근할 수 없는 셀이 생성될 때 수행되는 참조 카운터와 사용 가능한 공간 목록이 비어있을 때만 매립이 발생하는 마크 스위프라고 불립니다. 이 두 가지 방법은 때때로 열정적인 접근법과 게으른 접근법이라고 불립니다. 이 두 가지 접근법의 많은 변형이 개발되었습니다. 그러나 이 섹션에서는 기본적인 과정에 대해서만 논의합니다.
저장 회수의 참조 카운터 방법은 현재 셀을 가리키는 포인터의 수를 저장하는 카운터를 모든 셀에 유지함으로써 목표를 달성합니다. 참조 카운터에 대한 감소 연산에 포함된 것으로서, 포인터가 셀에서 분리될 때 발생하는 것은 0 값에 대한 체크입니다. 참조 카운터가 0에 도달하면 프로그램 포인터가 셀을 가리키는 것이 없음을 의미하며, 따라서 가비지가 되어 사용 가능한 공간 목록으로 되돌릴 수 있습니다.
Variable-Size Cells
가변크기의 셀9이 할당된 힙을 관리하는 것은 단일크기의 셀을 관리하는 데에 많은 어려움이 있지만 추가적인 문제점도 있습니다. 안타깝게도 대부분의 프로그래밍 언어에서는 가변크기의 셀이 필요합니다. 가변크기의 셀 관리로 인해 제기되는 추가적인 문제점들은 사용되는 방법에 따라 달라집니다. 마크 스위프를 사용하면 다음과 같은 추가적인 문제점들이 발생합니다:
• 쓰레기임을 나타내기 위해 더미에 있는 모든 셀의 표시기를 초기 설정하는 것은 어렵습니다. 셀의 크기가 다르기 때문에, 셀을 스캔하는 것은 문제가 됩니다. 하나의 해결책은 각 셀의 크기를 첫 번째 필드로 갖도록 요구하는 것입니다. 고정된 크기의 셀에 비해 약간 더 많은 공간과 다소 많은 시간이 걸리지만 스캔을 수행할 수 있습니다.
• 마킹 과정은 사소한 것이 아닙니다. 포인터가 들어가는 셀에 포인터의 위치가 미리 정의되어 있지 않은데 어떻게 포인터에서 따라올 수 있을까요? 포인터가 전혀 없는 셀도 문제입니다. 런타임 시스템에 의해 백그라운드로 유지되는 각 셀에 내부 포인터를 추가하면 됩니다. 하지만 이러한 백그라운드 유지 관리 처리는 프로그램 실행 비용에 공간 및 실행 시간 오버헤드를 모두 추가합니다.
• 사용 가능한 공간의 목록을 유지하는 것도 오버헤드의 또 다른 원천입니다. 목록은 사용 가능한 모든 공간으로 구성된 단일 셀로 시작할 수 있습니다. 세그먼트에 대한 요청은 단순히 이 블록의 크기를 줄입니다. 회수된 셀은 목록에 추가됩니다. 문제는 오래지 않아 목록이 다양한 크기의 세그먼트, 즉 블록의 긴 목록이 된다는 것입니다. 요청이 충분히 큰 블록에 대해 목록이 검색되게 하기 때문에 이것은 할당을 느리게 합니다. 결국 목록은 대부분의 요청에 충분히 크지 않은 매우 작은 블록의 수를 많이 구성할 수 있습니다. 이 시점에서 인접한 블록은 더 큰 블록으로 붕괴되어야 할 수도 있습니다. 목록에서 충분히 큰 첫 번째 블록을 사용하는 대안은 검색을 단축시킬 수 있지만 목록을 블록 크기별로 순서화할 것을 요구합니다. 두 경우 모두 목록을 유지하는 것은 추가적인 오버헤드입니다.
참조 카운터를 사용하면 처음 두 가지 문제는 방지되지만 가용 공간 목록 유지 관리 문제는 여전히 남아 있습니다. 메모리 관리 문제에 대한 포괄적인 연구는 Wilson(2005)을 참조하십시오.