3.0 Abstraction
High-Level Language
- Fortran, Algol(이후 영향 미침), Lisp과 같은 초창기 언어 → high level
- syntax(문법), semantic(의미)이 추상화됨: (어셈블리 언어에 비해)
- 컴퓨터로 해결하기 위해서 모두 고려하기 힘듦 → 추상화함
- 기계에 의존적이지 않음
- 사람이 프로그래밍할 때, 이해할 때에 더 쉬움
Names
- 뭔가를 나타내는 문자열
- a=3 : 3이 있는 공간의 이름
- l-value: 공간, r-value: 값
- 식별자로 사용 (alphabet + number)
- 변수, 상수, 연산자 등을 참조
- 복잡한 코드와 연결 (변수, 함수, 클래스)
Abstraction
- 특정 컴퓨터 구조의 자세한 내용으로부터 언어의 기능 분리시킴
- Subroutine, function - Control abstractions
- 복잡한 코드 부분은 숨기고 간단한 interface만 보이도록 함
- 실제로 호출하는 부분을 숨김
- Class - Data abstractions
- 클래스의 정보, 함수를 감춤
- 내부 구조에 대해서는 자세히 알 필요 없음
3.1 The Notion of Binding Time
Binding
- 이름과 이름을 나타내는 무언가를 연결
- 함수와 함수 코드, 변수와 메모리 공간
- Binding Time
- 바인딩이 언제 일어나는가
- 코드 작성, 컴파일, 링크, 로드, 메모리 올릴 때
- early binding time: 효율성 증대시킴
- later binding time: 유연성 증대시킴
Binding Time
- Program writing time
- Compile time
- if문을 작성했다(고급언어) → 기계어로 바꿈
- Link time
- 기존 함수(standard subroutines), 내가 만든 함수 합치는 과정
- ex) 기존 함수: print-system이 제공하는 함수 (언어마다 다른 모듈)
- 메인을 작성하다가 printf 사용 → 컴파일되어 있는 곳으로 가야함
- int a; → a라는 변수의 공간을 어딘가에 잡아둬야함, 내부에 존재 → link
- 따로 컴파일 된 것을 링크
- Load Time
- 운영체제에서 메모리에 코드, 데이터를 올려서 실제 실행할 수 있는 상태를 만들어 주는 과정
- Link time에 만들었던 address, load address 다름
- 가상주소 → link time, 물리주소 → run time
- 실제 메모리에 올렸을 때 그 때의 주소는 다름
- virtual(link) vs physical(run time에 바뀔 수도)
- Run Time
- 프로그램 처음부터 끝까지
- 바인딩 이루어 짐
- elaboration time: declaration이 처음 보여질 때
- storage allocation and binding process
- 함수 시작 시간, block entry 시작
Static & Dynamic
- Static: 컴파일 시간, link time
- Dynamic: run time
- 컴파일러 기반 언어는 인터프리터에 비해 효율적
- 인터프리터는 매번 변수 사용할 때마다 공간 확인해야 함, 사용할 수 있는지 (실행해봐야 알 수 있음)
- 컴파일러는 먼저 확인하고 지나감
- Early binding times(컴파일)/ static: 미리 확인하고 넘어가기 때문에 더 효율적, 대부분 컴파일 언어
- Later binding times(런타임)/ dynamic: 실행될 때마다 확인, 대부분 인터프리터 언어
Binding Time & Language Design
- 유연성이 높음 → 객체지향
- A a; A 상속 B
- a= new A(); a = new B(); a.뭐시기 → 실제 객체는 A 클래스인지 B클래스인지는 실행할 때 알 수 있음 / 실행때 바인딩을 해줌
- 실행되어야 알 수 있는 언어 존재
상속 및 오버라이딩
Late Binding:
- Parent c = new Child("child",2019);
- override 된 자식의 getName() 덮어씌우기 때문 → run time에 이루어진다.
3.2 Object Lifetime and Storage Managements
- Object(변수의 메모리 공간을 뜻함) Lifetime
- 이름과, 실제 그 이름이 나타내는 object를 구별해야 함
- key events
- 객체 생성과 소멸
- 바인딩 생성과 소멸
- 바인딩이 사라지고, 다시 살아남 (잠시 사용못하는 경우)
- 변수의 참조
- Binding's lifetime & Object's lifetime
- 바인딩이 만들어졌다가 소멸
- 이 둘의 라이프 타임이 일치하지 않을 수도 있음
- name-to-object binding이 object lifetime보다 길다
- 코드 자체가 잘못됨
- 간접적으로 바인딩(주소를 갖고 있는 것이므로)
- 예시 → dangling reference
- t의 주소는 담겨있지만 delete v 로 인해 object를 지우긴 했음
- 객체는 사라졌지만 바인딩은 여전히 있음
- object lifetime은 storage allocation mechanism 중 하나와 상응
- Static Object는 프로그램이 실행하는 동안 계속 남아있는 절대 주소(logical 의 절대를 의미함) : 전역 변수, data 영역
- Stack object: 함수가 실행할 때 사용되는 영역 (지역 변수)
- Heap object: 동적으로 할당되는 변수
3.2.1 Static Allocation
Static objects
- 전역 변수, data 영역
- static: static 변수의 경우 여러 번 호출시, 값이 변함
- 하드 코딩된 것들 (numeric, string-valued constant literals 상수, 문자열)
- 대부분의 컴파일러는 디버깅, 동적 타입 체크, 가비지 콜렉션 등을 테이블로 생성해서 저장 필요 → statical하게 할당
- Statically allocated object란 프로그램 실행 중에 값이 바뀌면 안되며, 수정하지 못하게 read-only memory에 보호되어 할당됨
특징
- 지역 변수들은 함수 호출시 생성되었다가 반환시 소멸 된다. (stack-based allocation)
- Fortran: Recursion은 지원하지 않음 (stack 이 없었으니까)
- 서브루틴 한 번만 호출 가능
- 컴파일러는 지역 변수에 대해도 static 할당 가능 → (장점) 다른 함수 호출하는데 같은 영역 재사용 가능, run-time overhead(스택 관리시 발생하는/ 만들고 삭제 등등 )를 줄일 수 있음 (메모리 효율적)
- 많은 언어에서 named constant는 컴파일 타임에 값이 결정되는 걸로 지정 → 이미 알려진 상수, 빌트인 함수, 산술 연산, 이미 정해진 값 정도만 사용
- C언어/ Ada에서는 상수라고 하는 것은 변수이지만 값을 바꿀 수 없는 변수임 (상수 아님) → elaboration-time constant : stack
3.2.2 Stack-Based Allocation
- Recursion is allowed
- frame, activation record: 한 개 함수가 호출되면서 쌓이는 영역
- 매개변수, 지역변수, return values, 리턴 주소(Bookkeeping), 나를 부른 곳 등등
- register: 빠른 메모리 공간, 이전에 사용하던 것들 값을 덮어 씌우면 안됨 → stack에 저장해 둠, 함수로 되돌아갈때 복원
- callee에서 쉽게 찾을 수 있으므로 프레임의 맨 위로 올라갈 수 있음 (언어마다 다름)
- 스택 관리: Calling sequence가 관리함
- caller에 의해서 호출되기 직전과 직후에 실행되는 코드
- Prologue: 호출 됐을때, epilogue: 호출 끝났을 때
- Stack Frame의 위치는 컴파일 타임때 알 수 없고 실행시 알 수 있다.
- frame안의 object의 offset은 알 수 있음
- stack pointer: 스택의 끝
- frame pointer: 현재 실행되고 있는 함수의 activation record 어딘가를 가리키고 있음, D의 호출이 끝나면 스택에서 빼와서 변경 → 상대적인 위치로 지역 변수에 접근한다.
- 지역 변수에 대한 내용은 오프셋을 파악하고 있기 때문에 이것을 기준으로 변수에 접근 가능
- 지역 변수의 경우:일반적으로 negative offset
- 리턴 값, 매개 변수: positive값 → sp 이용
- Recursion이 없는 경우 → static 사용, 스택을 사용하는게 장점이 더 많음
- stack 사용시 장점
- 모든 서브 루틴이 동시에 호출되지 않음, 모든 서브 루틴의 공간을 할당해놓을 필요는 없음→ 적은 메모리 사용 가능 (효율적)
- 재귀 가능
Heap-based Allocation
Heap
- 실행 중에 할당, 해제 가능 → 동적 할당 공간
- C++ →
malloc
, free
/ java → new
- dynamically allocated data structures에 사용
- String a = "abc" / a = "abcd" → heap이라고 볼 수 있음 (run time)
- 공간, 속도 낭비를 고려해서 처리하는 방법 전략 세움
- Internal Fragmentation → 블럭 크기 일정하다면 문제 심해짐
- 필요한 메모리보다 큰 영역을 가져가서 나머지는 버림
- External Fragmentation
- 요청한 크기는 있지만 연속적이지 않음
- 시간이 갈수록 원하는 크기의 메모리를 할당해주지 못할 수 있음
- 사용되지 않는 메모리를 단일 연결 리스트로 관리
- first fit: 크기와 상관없이 제일 먼저 나타나는 알고리즘 → 빠르다
- best fit: 원하는 크기와 가장 크기가 비슷한 것 → 공간 낭비 줄여줌 → O(n)
- 단일 연결
- 요구된 것보다 더 큰 메모리를 선택할 때, 사용하는 공간, 사용되지 않는 공간나뉘어짐 (free list)
- 연속적인 메모리를 합치는 경우도 있음
3.2.4 Garbage Collection
Heap-based object의 할당은 프로그램의 특정 연산자에 의해 일어남
- 객체 생성 (new)
- list.append()
- 짧은 문자열을 가지고 있던 변수에 긴 문자열을 저장할때
→ 해제의 경우 프로그래머가 요청(C, C++, Rust): Explicit
- Implicit 하게 처리 해줌 → Garbage Collection
Explicit Deallocation
- 장점
- 단점
- 에러 발생 (manual deallocation error)
- object가 빨리 deallocate한다면 dangling reference(허상 포인터)
- object가 deallocated되지 않으면 garbage, 공간 차지
Garbage Collection
- 장점
- 에러 없음
- run time overhead 줄어듬
- 최근에 복잡해져서 자동으로 정리해주는게 유용해짐
- 단점
- 언어 구현의 복잡성
- 프로젝트 내에서 시간을 소비함 → 자동으로 처리하는 것을 포함시킴 (데이터 생성, 바인딩 내용)
3.3 Scope Rules
→ 변수, 함수의 사용 범위
Scope of a binding
- a 변수와, a 변수 차지하는 공간 → data 영역(static int a=3;)과 이름 바인딩→ 바인딩이 유효한 영역
Statically scoped
- compile time → 코드를 보면 알 수 있다.
- 대부분의 현대 언어 (C, java, Python, C#)
Dynamically scoped
- run time → 실행해봐야 알 수 있다.
- 실해의 흐름에 따라 달라짐
- APL, Snobol, Tcl
Scope
- 바인딩이 변경되지 않는 코드상의 최대 영역
- 모듈의 바디, 클래스, 서브 루틴, if(){} 등 블럭
Referencing environment
- 실행될 때 특정 순간에서 사용할 수 있는 바인딩
- 변수, 전역 변수, 함수 등등
C 언어
- 코드 블록 시작 → 새로운 스코프 만듦
- 같은 이름을 사용하는 지역, 전역 변수 → 로컬에 대한 새로운 바인딩 만듦 (전역을 지역이 감춤) = 지역 변수를 우선시 함
- 종료 → 바인딩 소멸, 전역 변수를 위한 바인딩이 보이도록
3.3.1 Static Scoping
Static scoping
- 컴파일 때 결정되는 이름과 오브젝트 사이에서의 바인딩
- Current binding: 가장 가까이 선언된 변수 사용
- Early versions of Basic
- 초보자위해서 만듦
- 단일 스코프
- 파이썬 처럼 a=3 처럼 쓰면 선언되는 것처럼 명백한 선언은 없음
- pre-Fortran 90
- 전역, 지역 변수
- 지역 변수의 스코프는 서브 루틴 내에 제한
- 만약 변수가 따로 선언되어 있지 않으면 현재 서브 루틴에 지역적으로 사용
- 변수의 이름이 문자 I 부터 N까지 시작하면 integer, 아니면 real
- 지역 변수: 함수 내, 한번 사용하고 사라지도록 됨
- save를 사용해서 함수가 다시 실행되더라도 전에 실행했던 함수의 지역 변수 사용할 수 있도록 함
- c의 static → 처음 0으로 초기화 → 함수가 여러 번 호출 되더라도 값은 유지
- Algol의 own
3.3.2 Nested Subroutines
Nested Subroutines
: 함수 안에 또다른 함수
- python, Lisp 지원, C언어 후계는 지원 안함
- Algo-family: 특정 스코프에 정의되어 있는 상수, 변수, 함수는 스코프 밖에서는 모르도록 → 선언된 영역에서 사용
- 같은 이름 사용 → 바깥은 감춤
- active 변수 → 현재 위치로부터 가장 안쪽부터 찾음
- built-in 함수 제공 (입출력): 가장 바깥에 있음 (전역 부분)
- hole 존재 → 같은 이름
- p1의 x를 f1에서 접근하지 못하지만, p1.x or p1::X와 같이 사용해서 접근 가능하도록 함 (Ada, C++)
- lexically surrounding subroutines
- 로컬 → frame pointer와 offset 사용
- 전역변수는 아예 주소 정해져있어서 상관 없음
- offset 벗어난다면 어떻게 사용?
- 해당하는 프레임 찾아감 → 각각의 프레임 마다 부모에 해당하는 stack frame에 static link 걸어줌 null이 나올때까지 따라 올라가면 됨 k단계까지 내려감 → static link k개
- k단계만큼 내려갔다 → k만큼 런타임에 링크
- C → B → A (2번)
3.3.3 Declaration Order
블럭 b에 x 선언 → x의 스코프는 선언 이후부터 or 블록 전체?
- 초기 언어: 모든 선언을 scope의 시작에 두도록 (Algol 60, Lisp)
- 파스칼: 이름이 사용되기 전 선언 재귀 or subroutine 에서 유의해야 할 점 존재
- N이 사용되었는지 확인하려면 블록안의 전체 내용 확인해야함
- 전체를 유효 범위로 인식 → 선언 이후부터 인식으로 확장 (C, C++, Java, Ada)
- C++, java에서 변수 선언을 늦게해도 되도록 함
- 사용하기 전 define부터 해야한다는 것을 완화함
- declare 사용전 사용 가능 → 같은 클래스 내부에만 있으면 어디서든 접근 가능
- 클래스의 멤버는 모든 클래스의 메소드에서 사용가능
- C# 함수 내에서는 오류 발생, 지역변수는 파스칼처럼 whole block scope 따름 클래스 멤버 변수는 해당안함 → 앞에서의 자바 예시가 적용이 됨
- python: whole block variable declaration 자체가 없는 언어 ex) int x;
- x=3 을 사용할 것 같은데 whole block 을 사용하므로 x=5가 S() 전체에 영향 미침 → 따라서 오류 발생 (선언 전, 값이 존재하지 않는 상태에서 사용했기 때문)
global
사용해서 해결
Declarations and Definitions
recursive type, subroutine: 사용되기 전에 선언되어야 함
void list_tail(follow_set fs);
void list(floow_set fs) {
switch (input_token) {
case id: match(id); list_tail(fs);
…
}
void list_tail(follow_set fs) {
switch (input_token) {
case comma: match(comma); list(fs);
…
}
Nested blocks
- Algol60, C89(ANSI C), ada ⇒ 지역 변수 함수의 시작 부분에서 선언할 수 있음
- ANSI C만 지원하는 컴파일러는 함수, 블럭 첫 부분에 선언해야함
- C++, jaca, Algol 68, C99는 유연성 있게 어디서나 선언 가능, 해당 변수가 사용되기 전 선언되어야 함
- 대부분 언어는 nested declaration은 outer declaration을 감춰줌
- java, C#: static semantic error (outer declaration 이 현재 서브 루틴의 지역 변수라면)
- 동일 함수 내에서 동일 이름으로 또 선언되면 에러 발생
- c 언어 → 오류 발생하지 않음
- 임시 선언 → lexically adjacent (바로 붙여서 사용 가능)
- 코드가 너무 길다면 {} 써서 변수 선언 가능 → 프로그래밍이 쉬워짐
- 블럭 안에만 임시로 사용해서 쓰게해 줌
- 가독성 높임
- 해당 코드가 다른 temp라는 이름을 가진 변수를 간섭할 가능성을 배제해줌
- 일반 지역 변수와 라이프 타임이 비슷함 ⇒ 함수 실행할 때 생성, 함수 종료시 삭제
Modules
- client이 몰라도 되는 내용을 숨김
- 자동차 같은 경우, 운전하는 사람이 자동차 작동 방법은 알 필요가 없음
- cognitive load: 머리로 생각해야 하는 부분 줄여줌
- 인터페이스는 최대한 단순히, 내부 수정을 인터페이스에는 건드리지 말아야함
- information hiding
- 이해해야 하는 부분을 최소화하기 때문에 cognitive load (인지 부하) 줄여줌
- 이름 충돌 위험 줄여줌
- 데이터 추상화의 응집성을 통해서 데이터 보호할 수 있음 (데이터가 변경되지 않도록 하는 무결성 보장) → 데이터가 변경되지 않도록, 밖에 보이지 않으니까
- compartmentalize: run time 에러 발생할 때 특정 클래스, 모듈 내로 한정 시켜줌
Encapsulating data and Subroutines
void f(){
int b;
void g(){
int a;
}
}
- nested subroutine에 의해 제공되는 information hiding은 라이프 타임이 같음
- g는 f가 살아있어야 사용 가능 → f와 같은 라이프 타임 사용
- 지역 변수는 더 이상 메모리에 남아 있지 않는 문제 발생
- Solution
-
save나 static을 사용함
- 스코프는 함수 내지만 object 라이프 타임은 남아있음
- static: 함수 내부가 스코프 영역
- c언어에서 전역 static → 파일 내부만 사용 가능
→ 한 개 이상의 서브루틴으로 구성된 것에는 사용하지 못함, 다른 함수와 같이 사용하는 변수라면 의미가 없음
ex - pseudo-random number
- 시드 넘버가 같으면 결국엔 반복되는 패턴임
- 시드 넘버를 다르게 줘야함
- 함수 - 함수 넘나들도록 : rand_int, set_seed 에만 해당하는 변수 설정하고자 함
- Modules as Abstractions
- 모듈은 서브루틴, 타입, 값 등의 객체를 encapsulate를 가능하게 함
- 모듈 내의 함수는 서로 자유롭게 사용 가능
- 모듈 안의 함수들은 모듈 밖에 보이면 안된다 (export될 때까지)
- object의 접근성(사용 여부)에만 영향을 미치고 (모듈이 사용할 때만 object의 라이프 타임이 살아나고 등등 이런게 아님) 라이프 타임에는 영향을 미치지 않음
- python - package, java - module, package, C C# PHP - namespace
- C언어에서도 비슷하게 흉내 낼 수 있음 (static 함수 사용)
#include <time.h>
namespace rand_mod {
unsigned int seed = time(0);
const unsigned int a = 48271;
const unsigned int m = 0x7fffffff;
void set_seed(unsigned int s) {
seed = s;
}
unsigned int rand_int() {
return seed = (a * seed) % m;
}
}
- namespace 내부에서 만들어진 name 바인딩은 밖에서 부분 으로 감춰짐 → 사라지는 것은 아님
- C++: namespace는 전역보다 바깥에 위치해야 함(outermost)
- set_seed 와 rand_int에 관계 없이 seed 값은 유지됨 rand_mod::set_seed 로 사용할 수 있음 (:: 속해 있다 → import의 의미로 쓴다)
- c++ 에서 export, import(사용하는 곳) 없음
using rand_mod::rand_int
사용해서 이후부터 extern 없이 rand_int
사용 가능
using namespace rand_mod
: rand_mod의 함수를 모두 사용한다
- but 충돌 가능성 있음
Imports and Exports
- 변수는 읽기 전용으로 export 될 수 있다
- type - opaquely: 타입이 명확하게 알려지지 않은 export
- Closed scopes
- Open scopes
- Selectively open modules
- rand_mod::set_seed와 같이 사용
- import, export push, pop 사용
- int, double 타입의 스택을 만들거다 → 모듈 안에 지정하면 그 타입 밖에 못 씀, 타입을 밖에서 지정해서 좀 더 유연성 있도록
Modules as Managers
- 모듈은 추상화된 타입을 생성하는 것을 허용함. (= 서브 루틴에 Private 한 데이터 포함)
- 스택의 배열의 공간 확보: 내부적으로 사용하는 것임
- 단점: single abstraction만을 정의할 수 있음, 모듈 여러개 사용 못함
- element 밖에 둬서 다른 타입을 element 사용 가능 → element 를 밖에서 지정 → 한 가지 종류의 스택만 생성 가능, 여러 개의 stack instance 생성 불가
- Solution
- 모듈의 코드를 복사해서 다른 이름으로 새로 만듦 → stack2, real_stack 등등
- 매니저 개념으로 만들어야함
- 모듈을 타입으로 지원하자
- 클래스와 비슷
- 클래스는 상속을 활용해서 기존의 클래스를 활용해 확장 함수를 만들 수 있음
- 클래스는 다형성 지원
- dynamic method dispatch: 부모의 함수를 호출하는 것 처럼 보이지만 자식의 함수 호출
- 모듈 클래스 특징 비교
- 모듈: client가 몰라도 되는 내용을 숨기며, 지역 변수 static으로 선언한다면 스코프가 함수로 한정되는 단점이 있어 서브 루틴 간의 값을 공유하지 못한다. 이의 단점을 보완하기 위해 모듈이라는 개념이 등장하게 되었다.
Modules Containing Classes
3.3.6 Dynamic Scoping
- 실행에 따라 달라짐
- 가장 최근에 마주친것을 current binding
- 인터프리터 방식을 채택하는 경우가 많음
- Static Scoping VS Dynamic Scoping
- static: 해당 변수가 스코프 내에 없을 때 가장 가까운 바깥쪽 부터
- Dynamic: 실행 중에 만났던 가장 최근의 변수 (실행되는 스코프에서 리턴되면서 사라지지 않은 상태의 변수여야 함) (APL, Snobol, Tcl, TEX, early dialects of Lisp, Perl)
- dynamic의 second()의 int n을 1로 바꿈
- second() 함수의 지역변수는 1로 바꼈다가 리턴되면서 사라지므로, 전역 변수 n=2가 된다
- first()는 전역 변수 1로 바꿈
- 가장 가까운 double max_score를 가져감
- 타입이 안맞는 경우가 발생할 수도 → type crash
Implementing Scope
- statically scoped program에서 compiler는 symbol table을 만듦
- a name-to object-binding: 새로운 매핑 추가
- look up: 저장된 이름 조회
- stastic(compile) - stack 사용 (enter_scope, leave_scope) → 가장 위에 있는 a
- dynamic(interpreter) - 최근이용