0. 개요
🤔 궁금증 or 들어가기 전
특정 name이 선언되기 전에 사용이 가능한가에 대한 이야기이다.
// C++ 이라고 가정
int main() {
int n = 0;
n = x + 5; // 가능한가?
int x = 3;
printf("value = %d\n", n);
}
main 함수 내에서 일어나는 일이라고 가정, 선언 전 Line에서 x가 사용 가능하냐는 의미
결론부터 말하자면, 불가능
=> 정적 의미론 오류 (Static Semantic Error)
오히려 x가 이전, 밖에 먼저 선언되어 있으면 가능
1. 각 언어의 선언 순서
1-1. Algol 60 , Lisp (초기 언어)
- 모든 선언이 해당 스코프의 시작부에 나타야 한다.
1-2. Pascal
- declare-before-use rule : 변수나 이름이 사용되기 전에 선언부터 해야한다.
- declare-before-use rule지키지 않을 시, a forward reference (이름 선언 전 사용 시도) -> Static Semantic Error
- whole-block scope rule : 식별자의 스코프가 선언된 블록 전체를 나타낸다.
const N = 10;
...
procedure foo;
const
M = N; (* 정적 의미 분석 오류! *)
...
N = 20; (* 로컬 상수 선언; 외부 N을 숨깁니다 *)
declare-before-use rule와 whole-blcok scope rule 라는 두 가지 개념이 오묘하게 충돌하는 경우이다.
-> 라인 순서대로만 따라간다면, M=N 에서 전역변수 N=10을 참조할 수 있을 것 같다.
한 스코프 내부에서 선언이 있었다면 해당 선언의 스코프가 블록 전체에 작용하는 행위가 우선시된다.
같은 scope 내의 N을 사용하여, 외부 N이 먼저 가려지게 된다.
So, declare-before-use rule을 어긴 것으로 처리된다. => ERROR
const N = 10;
procedure foo;
const
M = N; (* 정적 의미 분석 오류! *)
var
A: array[1..M] of integer;
N: real; (* 외부의 선언을 가림 *)
- const 키워드를 사용해서 M이라는 상수를 선언하고 N으로 초기화하려는 시도
- 스코프 내부의 real이라는 변수 선언으로 인한 외부 N은 가려짐.( whole-blcok scope rule )이 먼저
- 🚨 N이 상수가 아니고 변수라 에러가 나는 게 아니라
" 선언 이전에 사용했기 때문 "
-> 정적 의미 분석 오류
1-3. Pascal 이후의 언어들 ( Ada, C, C++, Java ... )
Pascal은 해당 스코프 선언 부분을 스캔하여 해당 이름이 숨겨졌는지 확인했다. ( whole-blcok scope rule )
Pascal 이후부터는
식별자의 스코프가
선언된 전체 블록 (X)
"선언된 순간부터 블록 끝까지" (O)
ex. Ada, C, C++, Java 등의 언어
다시 처음에 봤던 코드를 보자.
// C++ 이라고 가정
int main() {
int n = 0;
n = x + 5; // 가능한가?
int x = 3;
printf("value = %d\n", n);
}
int x = 3;는 그 라인부터 함수가 끝날 때까지 유효하고 해당 라인 이후부터 사용할 수 있다.
그렇다면 다음 예시를 보자.
외부 선언 x
{ x 사용 ... (1)
내부 x 선언
x 사용 ... (2)
}
라인 순서대로 스코프가 생기므로,
2번 라인에서는 외부 선언 x를 사용하고
3번 라인에서 내부 x가 선언된다. 그리고 외부 x는 가려지고 해당 바인딩이 유효하게 된다.
그래서 4번 라인에서는 내부 x를 사용하게 되는 것이다.
(1)은 외부 x, (2)는 내부 x
* Class 멤버 변수 선언 에서의 define-before-use 완화
// Java
class Test {
void print() {
System.out.println("value = " + value);
}
int value = 5;
}
C++이나 JAVA의 경우, 클래스의 멤버 변수 선언을 조금 완화시켰다.
클래스 내의 멤버 변수는 해당 클래스 내부에서 선언 이전 선언이 가능하다.
// Java
class ChildClass extends ParentClass {
...
}
class ParentClass {
...
}
JAVA의 경우, 클래스 순서마저도 순서 상관이 없다.
C++은 안된다.
1-4. C#
우리가 Pascal에서는 declare-before-use rule와 whole-block scope rule을 지켰고,
C, JAVA 등의 언어들에서는 declare-before-use rule은 적용되지만, whole block scope rule 은 폐지시켰다고 했다.
또한, 메서드 내부에서는 여러 완화 조항들도 있었다.
그런데 훨씬 늦게 나온 C#에서는 다시 whole block scope rule 을 살려버렸다.
교수님은 시대를 역행한 언어라고 말씀하실 정도....
class A {
const int N = 10;
void foo() {
const int M = N; // uses inner N before it is declared
const int N = 20;
// ...
}
}
C 혹은 JAVA의 관점에서 볼 때, M=N에서 외부 N을 사용할 수 있을 것 같지만
whole block scope rule 로 인해 declare-before-use rule 어긴 것이 되어버려 사용하지 못하게 된다.
1-5. Python
파이썬도 whole block scope rule 을 살려버렸다.
또한 python은 declare의 개념이 없고 사용하는 동시에 만들어진다.
외부 scope의 변수를 사용하려면 "global" 키워드를 사용해야 한다.
예시 1)
def T():
x =3
print(x) ... (1)
def S():
print(x) ... (2)
x=5
print(x) ... (3)
S()
print(x) ... (4)
T()
3 ... (1) 출력
3
5 ... (3) 출력
3 ... (4) 출력
// 위는 예상 출력
예시 2) 전역 변수를 사용하고 싶다면 global을 써라.
x= 3
def T():
global x
print(x)
x =5
print(x)
T()
3
5
만약 x=5라는 선언이 없다면? global x 없이도, 위의 print(x)는 전역 변수 x=3을 사용.
But Read-Only
왜냐? write를 하는 순간 예시 1과 같은 문제 발생.
2. 선언(Declartions)과 정의(Definitions)
< Class 멤버 변수 선언 에서의 define-before-use 완화 > 에서 처럼
우리는 이전이 계속 delcare과 define을 혼용해서 썼지만,
이는 엄연히 구별하면 다른 용어이다.
🤔 만약에 재귀적인 (recursive) 유형 등, 이름이 사용되기 전에 선언되어야 할 때는 어떻게 처리할까?
2- 1. 선언과 정의의 분리
이를 테면,
닭은 달걀을 낳는 함수와
달걀은 닭으로 큰다는 함수가 있을 때
어떻게 이 구현을 해결할 수 있냐는 것이다.
C와 C++에서는 객체의 선언과 정의를 구분하여 사용함으로써 해결한다.
선언 (declaration) : 이름을 소개하고 그 범위(스코프)를 나타낸다. <구현 세부 사항은 생략할 수 있다.>
정의 (definition) : 컴파일러가 해당 객체의 구현 세부 사항을 결정할 수 있을 정도로 상사하게 객체 설명!
-> Mutually recursive (상호 재귀) 문제 해결
2-2. 중첩 블록 (Nested blocks)
많은 언어들 (Algol60, C89(ANSI C), Ada...) 에서 지역변수의 선언은
서브루틴의 시작부 뿐만 아니라 아무 {...} 블록의 상단부에도 위치할 수 있다.
(C99와 같은 언어는 statement가 나타나는 어디에도(중간부) 선언 가능 -> 유연성)
Java와 C#과 같은 언어는 현재 서브루틴 내부의 지역 선언에서 외부의 선언을 가리키면 이를 정적 의미 분석 오류로 처리함.
같은 함수 내에서 같은 이름을 가진 선언이 두 개 있으면 안됨.
즉, 이러한 언어에서는 같은 이름의 변수가 서로 가려지면 컴파일 시 오류가 발생.
// Java
class Test {
int x = 7;
public void f() {
int x = 5;
{
// int x = 7; // compile error
System.out.println("1. x = " + x);
}
System.out.println("2. x = " + x);
}
}
cf. 사실 저렇게 중간에 아무 의미 없는 { } 블록을 만드는 것부터 안된다.
(C는 다 됨~)
+ 코드에 만약 if문이 있다면, 정적으로 이미 stack에 공간이 할당되어 있다.
(실행이 되지 않아도)