PL - 2.3 변수 유효 범위(Scope) 및 기타

Scope

Scope : 어떤 변수의 Visible한 코드 영역(Range of Statement)

Scope : 어떤 변수를 Reference할 수 있는 영역, 접근해도 에러가 발생하지 않는 코드 영역

  즉, Scope는 공간적인 개념이다.


Static Scoping

  C언어와 같은, 대부분의 언어에서 채택하고 있는 방식으로, 변수의 Scope가 Static하게 정의되는 방식이다. by Compiler

Static Scoping은 "어디서 선언했느냐"에 주목한다.

  • Local Variable : 당연히 로컬 변수를 선언한 Unit에서만 사용할 수 있다.

  • Nonlocal Variable : 자기 자신 Unit에서 사용할 순 있지만, 선언은 해당 Unit이 아닌 다른 곳에서 이뤄진 변수를 의미한다.

    • Global Variable이 Nonlocal Variable에 포함된다.

    • 또는, 자기 자신 Unit(함수)을 감싸고 있는 더 큰, 외부의 Unit(함수)에서 선언된 변수도 포함된다.

int a;

int main(void) {
	int a, b;
    
    while(...) {
    	int a;				// 이 a는 while문 내에서 선언된 a를 가리킨다. (Local)
        a++;				// Static Scoping
        b = 3;				// Nonlocal Variable for while()
    }
 	return 0;
}

~> 만약, while문에서 a를 선언하지 않는다면, Static Scoping 논리에서는, main에서 선언된 a를 가리키게 된다.
~> 그리고, main에서도 a를 선언하지 않는다면, Static Scoping 논리에서는, Global Variable a를 가리키게 된다.


  • Static Parent와 Dynamic Parent 개념
    • Static Parent : 자신(변수/함수 등)을 "선언"한 개체
      • 선언은 한 번만 가능.
    • Dynamic Parent : 자신(변수/함수 등)을 "호출"한 개체
      • 언제든지 바뀔 수 있음.

이 Parent 구분 기준은 Static / Dynamic Scoping의 구분 기준과 같다.

  아래의 Pascal 코드를 보자.

procedure big ;
	var x : integer ;

	procedure sub1 ;			// Inner(Nested) Function : 로컬 변수처럼, 로컬 함수를 정의!
		begin
			... x ....			// 이때, 이 x는 어디의 x인가.
		end ;

	procedure sub2;
		var x : integer ;
		begin
			sub1 ;
		end ;

	begin
		sub2 ;
	end 

~> big이라는 서브루틴 안에는 변수 x, 로컬 서브루틴 sub1, sub2가 선언 및 정의되어있다.
~> big이 sub2를 호출한다. sub2는 sub1을 호출한다. 이때, sub1에 있는 x라는 변수는 어디의 x일까?
~> sub1을 선언한 것은 big이므로 big이 Static Parent이다.
~> sub1을 호출한 것은 sub2이므로 sub2가 Dynamic Parent이다.
~> 즉, Static Scoping 언어라면, x는 big에서 선언한 x를 가리키게 된다. ★


Scoping은 컴파일러가 수행한다.


Nested(Inner) Function 기능 : 함수 내에 또 다른 함수를 선언 및 정의할 수 있는 기능. C언어에서는 지원하지 않으며, Ada, JavaScript, Python, FORTRAN 2003 등에서 지원한다.


※ Block : 코드 블록을 단위로 Scope을 따지는 언어도 있다. (최근 경향)
~> 코드 블록 내에서 로컬 변수 사용이 가능한 언어이다. ex) C언어
~> Static Scoping에 해당한다. 선언한 것이 기준이므로.
ex) if (a >= b) { int temp ; temp = a ; a = b ; b = temp; }


C는 Inner Function 기능은 없고, Block을 기준으로 하는 Static Scoping 방식이라고 보면 된다.


  • Static Scoping의 단점

  • 위의 그림의 구조로 프로그램을 짰을 때, Static Scoping 방식의 언어에서는 컴파일러에서 에러를 감지하지 않고 정상 통과시킨다.

    • 사실, 프로그래머가 주의한다면, 에러가 없다고 볼수도 있는 상황이다.

      • 하지만, 위의 구조는 분명 (논리적인) 에러 위험이 높다고 판단된다.
    • 1) E 안에서 Call D를 할 수 있을까?

      • 할 수 없다. D를 선언한 것은 A이고, E와 A는 Scope가 접점이 없기 때문이다.
      • E에서 D를 선언했는가? No
      • B에서 D를 선언했는가? No
      • Main에서 D를 선언했는가? No ~> Nope!!
    • 2) E 안에서 Call B는 가능한가?

      • 놀랍게도 할 수 있다. E 내부에 B 선언이 없는대도 말이다.
      • E에서 B를 선언했는가? No
      • B에서 B를 선언했는가? No
      • Main에서 B를 선언했는가? Yes ~> 이래서 가능해진다.
    • 3) D 안에서 Call B는 가능한가?

      • 역시 놀랍게도 할 수 있다.
      • D에서 B를 선언했는가? No
      • A에서 B를 선언했는가? No
      • Main에서 B를 선언했는가? Yes ~> 이래서 가능해진다.
    • 4) E 안에서 Call E는 가능한가?

      • 역시나 가능하다. 재귀호출인 것인데, 흐름은 아래와 같다.
      • E에서 E를 선언했는가? No
      • B에서 E를 선언했는가? Yes ~> 가능하다.

즉, 프로그래머는 A 안에 C, D를 선언해서 사용하고, B 안에 E를 선언해서 사용할 생각으로 위와 같이 프로그램을 짰는데, 의도치 않은 호출 관계가 가능해진 것이다.

Static Scoping에서 '선언'에만 주목하기 때문이다.

※ C/C++의 경우 Inner(Nested) Function 기능이 없고, Block 단위의 Static Scoping이 이뤄지는 언어이기 때문에 이런 문제점이 드러나지가 않는 것이다.

※ 한편, 위의 그림에서 B에서 A 호출은 Desirable 상황에서도 가능함에 주목하자. Top-Down 방식의 Imperative 언어이기 때문이다.
~> 마찬가지로, D에서 C 호출은 가능하다. 반면, C에서 D 호출은 불가능하다. Top-Down!


Dynamic Scoping

Dynamic Scoping은 '누가 선언했느냐'가 아니라, '누가 호출했느냐'를 따라간다.

Dynamic Scoping is based on the 'Calling Sequence' of subprograms!

  • 즉, Dynamic Parent를 따라간다는 것이다.

  • 따라서, Dynamic Scoping 방식에서 Scope는 Run-Time에 이뤄진다.

    • Static Scoping이 Compiler에 의해 Static하게 이뤄진 것과는 대조적이다.
      • Dynamic Scoping은 Interpreter가 적합하다.

  • Dynamic Scoping의 장점

    • 파라미터 패싱을 하지 않더라도, 자기(서브루틴)를 호출한 함수의 Local Variable들을 사용할 수 있다. 즉, Convenient하다.
  • Dynamic Scoping의 단점

    • Nonlocal 변수에 접근할 때 정적인 Type Check를 할 수 없다. 호출 흐름을 그대로 똑같이 따라해서 Type을 알아내야하는 것이다.

    • '텍스트 근접성(Textual Proximity)'와 상관없이 Visible한 서브프로그램의 로컬 변수는 모두 접근할 수 있다.

      • 그말은 즉슨, Reliability가 떨어진다. ★

여담) 실제로, 상기한 단점들이 너무 치명적이기 때문에 Dynamic Scoping을 적용한 언어는 현대에는 다 사장되었고 거의 없다.
ex) 과거에 Dynamic Scoping을 적용했던 언어 : APL, SNOBOL, LISP의 일부 Dialect 버전


Pascal 언어 예시

  Pascal 언어를 예시로 보면, 다음과 같다. Procedure 내의 Local Variable에 대한 내용이다.

  • Scope of Pascal : Declaration부터 end 예약어까지
  • Lifetime of Pascal : Procedure 시작부터 end 예약어를 만나 종료하기까지

~> Pascal은 Static Scoping 언어이다.

procedure example;
	procedure increments;
		begin
			sum++;			// Static Scoping에선 에러, Dynamic Scoping에선 가능
		end;				// Pascal은 Static Scoping이므로 에러이다!

	procedure computes;
		var sum : integer;
		begin
			increments;
		end
	begin
		computes;
	end;

~> Static Scoping 기준으로 보면, increments 함수 내에서 sum이란 변수는 Invisible하다.

하지만, 이때 주목할만한 점은, Scope는 Invisible이지만, sum의 Lifetime은 increments에서 유효하다.

왜냐? sum은 computes 프로시저에서 선언되고, 해당 프로시저는 increments 프로시저를 둘러싸고 있기 때문이다.

즉, Scope와 Lifetime은 구분해야한다. Lifetime은 "선언부터 종료"까지이다.


Referencing Environment

Referencing Environment : 특정 Statement에서 Visible한 모든 Name들의 집합을 의미한다.

  • Static Scoping 언어에서 Referencing Environment는

    • 해당 변수가 선언된 Local Scope와, 그 Scope의 Ancestor Scope들에 있는 모든 변수들
  • Dynamic Scoping 언어에서 Referencing Environment는

    • 해당 변수가 선언된 Local Scope와, 그 프로시저가 거슬러 올라가는 호출 흐름 상에 있는 모든 변수들
  • 이때, 두 방식 모두, 같은 Name이 있는 경우, Scope 관점에서 더 가까운 변수만 취급하고, 먼 변수는 Hidden Variable로 취급한다. ★

  • Active Procedure : 프로시저의 수행은 시작되었는데, 아직 종료되진 않은 프로시저


  아래의 Pascal 언어 예시를 보자.

procedure example ;
	var a, b : integer ;
	procedure sub1 ;
		var x, y : integer ;
		begin
			....  		// 여기서는 sub1의 x, y, example의 a, b를 접근 가능 (Static Scoping)
		end ;

	procedure sub2
		var x : integer;   		// ⓐ 지역 입장에선 sub2의 x가 Hidden Variable이다.
		procedure sub3 ;
			var x : integer ;
			begin 
				....       // ⓐ 지역. 여기서는 sub3의 x, example의 a, b 접근 가능 (Static)
			end
		begin
			.... 		// 여기서는 sub2의 x, example의 a, b만 Reference 가능 (Static)
		end
	begin
		....			// 여기서는 a, b만 접근 가능 (Static Scoping)
	end ;

~> 주석을 읽어가며 흐름을 잘 이해해보자.


Named Constant

Named Constant : Storage Binding이 일어날 때 딱 한 번만 Value Binding이 일어나고, 그 이후에는 값이 변하지 않는 변수

  • C언어의 #define 전처리문은 여기에 해당하지 않는다.
#define MAX_LEN		100000		// 얘는 변수가 아니다. 그냥 바꾸는거지.
  • C언어의 const 키워드 선언이 된 변수가 바로 Named Constant의 예시이다.
    • const 선언 시, 초기화 이후 값을 바꾸려고 시도하면 컴파일러가 컴파일 에러를 내보낸다.
const float PI = 3.141592;
PI += 3;					// Compile Error since it's Named Constant !
  • 이러한 Named Constants는 프로그램의 안정성, 신뢰성(Reliability)을 높이는 목적을 가진다. ★

Initialization

Initialization : Storage Binding이 일어날 때 Value Binding을 수행하면, 그것이 바로 초기화(Initialization)이다.

  각종 언어의 초기화 방법
1) C
int a = 3;
const int a = 3; (Named Constant)

2) FORTRAN
REAL PI
INTEGER SUM
DATA SUM /0/, PI /3.14159/

3) Ada
SUM : INTEGER := 0 ;

4) ALGOL ★
int first := 10;
int first = 10; (Named Constant)

좋은 웹페이지 즐겨찾기