-
프로그래밍 언어 Names, Binding, Scopes프로그래밍언어 2020. 5. 1. 17:38728x90
프로그래밍 언어에서 Names 라 하면 변수, Label, 함수, 키워드 등에 사용됩니다.
Names
Design issues
변수명의 최대 길이는 얼마나 길게 할 것인가?
언더바(_) 와 같은 Connector character 를 허용할 것인가?
대소문자를 구분할 것인가?
키워드나 예약어를 변수명으로서 허용할 것인가?
Name forms
이름의 형태에 제약이 있는 언어들이 있었습니다.
포트란 같은 언어들은 6글자, 30자 제한이 있었고, SQL 구문의 경우에는 대소문자를 구분하지 않는 등 다양한 형태가 있죠
물론 제가 주로 사용하는 C/C++, java 에서는 예약어를 사용하지 않는 한 특별한 제약은 없습니다. ( 숫자로 시작하면 안된다거나 그런거를 빼면요 )
Special words
키워드(keyword) : 프로그래밍 언어의 특정 상황에서 특별하게 동작하는 단어 예) int, float, if 등
예약어(reserved word) : 이름으로 사용할 수 없는 특별한 단어
C언어에서는 모든 키워드 = 예약어라서 둘이 뭐가 다른지 헷갈렸는데
키워드긴 하지만 예약어가 아닌 경우 해당 키워드는 변수명으로 쓰일 수 있다는거죠.
가령 예를 들어 if 가 키워드지만 예약어가 아니라고 친다면 if 를 변수명으로 만들 수 있다는거죠.
대신 변수 if의 scope 내에서는 if 문을 쓸 수는 없을거에요 왜냐면 if 는 조건문이 아닌 변수를 가리키는 놈으로 변했기 때문이에요!
scope 에 대해서는 조금 있다가 다뤄볼게요~
변수 (Variables)
변수는 컴퓨터 내의 메모리 조각(cell) 혹은 조각들의 모음에 불과해요
주소 (Address)
주소, 즉 메모리의 어디에 변수가 할당될 것인가에 대한 속성이에요. 모든 변수는 메모리 주소를 갖죠. (레지스터 변수 제외)
주소값을 lvalue(Location Value) 라고도 불러요
Alias(별칭) 는 C++ 를 하셨으면 익숙한 개념일거에요
int b = 10; int& a = b; a = 20; printf("%d", b); // 20
b 라는 변수의 별칭으로 a 를 만든거죠. 이런 경우 a는 새로운 메모리를 할당받는것이 아니라 b의 메모리를 공유하는 이름만 생겨난 셈이에요
이게 유용한 이유는 메모리를 절약할 수 있다는거죠. 가령 예를 들면 매우 큰 크기의 struct 를 함수 파라미터로 넘겨야할 때 C는 기본적으로 깊은 복사를 하여, Call by value 를 하기 때문에 호출 때마다 많은 복사연산이 필요해요. 하지만 별칭을 사용하게 되면 복사 하지 않고 넘길 수 있기에 유용합니다.
다만 단점으로는 프로그래밍 자체가 이해하기 어려워질 수 있어요. 생각치도 못한 곳에서 값의 변경이 이루어 질 수 있기 때문이에요
참고로 C언어에서도 Union, Pointer 등이 별칭에 속한답니다.
union MyUnion { MON, // 상수 0의 별칭 TUE // 상수 1의 별칭 }; int a = 10; int *b = &a; *b = 20; printf("%d", a); // 20
값 (Value)
value 라고 하면 메모리 조각(cell)에 담긴 값을 말하기도 하고, 변수 자체를 말하기도 합니다.
cell은 Physical cell 과 abstract cell 로 나뉘는데
Physical cell 은 바이트 단위, abstact cell 은 변수 그 자체를 단위로 한다고 이해하시면됩니다.
int a 의 Physical cell 은 4개의 byte 이고, abstract cell 은 a라는 변수 자체를 말합니다.
lvalue
객체(object) : 관찰, 참조 혹은 저장이 가능한 메모리 영역
lvalue : 객체를 참조하는 수식(expression)
rvalue
lvalue 가 아닌 모든 것
lvalue, rvalue 에 대해서는 나중에 자세히 다뤄보겠습니다.
타입 (Type)
타입은 변수의 범위와 변수가 처리할 수 있는 연산의 종류를 결정합니다.
이는 변수의 타입이 정의하는 것일 뿐 프로그래머가 직접 하지 않습니다. 다만 사용자 정의형(struct)를 만드는 경우에는 프로그래머가 해줘야겠지요
바인딩 (Binding)
지금까지 변수의 속성에 대해 알아보았는데요, 바인딩이라함은 이러한 속성들이 결정되는 것을 말합니다.
Binding time : 바인딩이 일어나는 시기. 즉 변수의 속성이 결정되는 시기
바인딩 시점
언어 설계 타임 (Language design time)
연산자의 정의가 언어 설계 시 정의됩니다.
* 는 곱하기 연산을 의미한다. 와 같은 정의를 말합니다.
언어 구현 타임 (Language implementation time)
컴파일러 제작 시라고 생각하면 됩니다.
int 는 4바이트를 갖는다와 같은거죠. int 형이 정수형을 의미한다는건 언어 설계 시에 정해지지만
구체적으로 몇 바이트를 갖는지는 컴파일러마다 다르기에 설계 시에 정해지는 속성이 아닙니다.
컴파일 타임 (Compile time)
변수의 데이터 타입이 컴파일 타임에 정해집니다. 구문/어휘 분석을 거친 뒤 변수명과 타입을 알아내게 되는것이죠
int a; -> 아 a라는 변수는 int 형이구나~
링크 타임 (Link time)
다른 .c 파일 .o 파일 등과 연결하는 것을 링크라고 합니다. 즉 라이브러리 같은 서브 프로그램을 호출하거나 extern과 같이 다른 파일에서 주입 받는 변수 등의 초기화가 링크타임에 이루어집니다.
로드 타임(Load time)
프로그램이 메모리 상에 올라가는 것을 Load 라고 합니다.
즉 프로그램이 시작되기 직전에 전역변수 할당 및 초기화가 Load time 에 이루어집니다.
예시
int a = b + 3;
int 의 정수형이라는 의미, int 에 들어갈 수 있는 값의 종류는 언어 설계 시간에 바인딩
int 가 4바이트라는 것은 언어 구현 시간에 바인딩
a가 int 형이라는 것은 컴파일 시간에 바인딩
= 이 배정문이라는 것은 언어 설계 시간에 바인딩
+ 연산의 의미는 양 쪽 피연산자를 파악한 뒤에 결정되므로 컴파일 시간에 바인딩
3 리터럴을 표현하는 방법은 언어 설계 시간에 바인딩
a의 값은 실행 시간에 바인딩
바인딩 종류
정적 바인딩 (Static binding)
컴파일 타임을 포함한 런타임 이전의 모든 바인딩으로, 프로그램이 실행되더라도 변하지 않는 속성들이 여기에 해당됩니다.
예) 변수의 타입
동적 바인딩 (Dynamic binding)
런타임 바인딩으로, 프로그램이 실행 될 때마다 바뀌는 속성들을 말합니다.
예) 변수의 주소값
타입 바인딩 (Type binding)
변수의 이름에 타입을 어떻게 명시하고 언제 할당되는지에 대한 이슈
정적 타입 바인딩 (Static type binding)
명시적(explicit) 선언과 묵시적(implicit) 선언으로 나뉘는데
명시적인 방법은 C언어에서 쓰는 방식으로, int a; 와 같이 그 타입을 직접 써주는 방식입니다.
묵시적은 Fortran 에서 변수 명이 I, J, K, L, M, N 으로 시작하는 변수는 int 다. 라는 암묵적인 규칙이 있어서 언어가 이를 인식하는 것입니다.
우리가 for 문을 돌릴 때 ijk 를 많이 쓰는게 여기서부터 유래되었다고 하네요~
장점
안전한 프로그래밍이 가능하다.
성능이 좋다.
단점
유연하지 못하다.
동적 타입 바인딩 (Dynamic type binding)
요즘 핫한 파이썬이나 자바스크립트에서 많이 쓰는 방식이죠? var a = 10; a = "abc"; 와 같이 타입을 명시하지 않고, 런타임에 변수의 타입이 정해지는 방식을 말해요. 이런 경우 언어의 타입 추정(Type inference)에 의존적이라고 합니다.
장점
유연하다.
단점
런타임 타입체킹으로 성능이 떨어진다.
위험하다.
// var a = int // var b = array func (var a, var b) { var c = a + b; // 컴파일 타입에 연산의 유효성 검사가 불가능 }
저장공간 바인딩 (Storage binding)과 생명주기 (Life time)
변수 명에 메모리 cell 을 바인딩하는 이슈
변수가 어느 공간에 할당되는지에 따라 그 생명주기가 달라집니다.
생명주기 : 변수가 특정 메모리에 할당되어 유지되는 기간우선 메모리 공간에 대해 간단히 알아야하는데요
원래 여기에 Data 영역도 있는데 Stack 의 가장 밑부분, 즉 배열로 따지면 0번째 인덱스가 Data 영역이에요
프로그래밍 공부할때는 빼놓아선 안되는 영역이지만 지금 당장 쓰이진 않아서 그리지 않았어요
TEXT 영역에 함수 같은 프로그램 소스 그 자체가 올라가게 되구요.
constants 영역에는 상수들이 들어가요. 예를 들면 char *str = "abc"; 이런게 문장이 있다고 합시다. C언어는 "abc" 라는 문자열을 배열로써 다루기 때문에 상수더라도 어느 메모리 공간에 위치되어있어야해요. 그럴때 임시적으로 위치하는 공간이 constants 영역이에요
Heap 은 동적할당, stack 은 함수 호출스택을 말하는거죠
정적 방식 (static)
전역변수나 static 변수가 정적 방식으로 생성됩니다.
프로그램이 시작되기 전에 항상 같은 위치에 할당된다고 해서 정적 방식이라고 불려요. 정적 방식으로 할당된 변수는 프로그램이 종료될때까지 할당 해제되지 않아요.
지역변수도 정적 방식으로 잡는 언어들은 메모리를 최적화한다는 장점이 있지만 함수를 재취호출 할 수 없다는 단점이 있어요
스택 동적 방식 (stack-dynamic)
지역 변수가 스택 동적 방식으로 바인딩이 돼요. 함수가 호출되어 콜 스택(혹은 runtime stack)에 쌓였을 때 메모리에 할당되고, 함수 동작을 끝마치고 종료되어 콜스택에서 빠져나갈때 메모리에서 할당해제되는거죠
명시적 힙 동적 방식 (explicit-heap dynamic)
C에서 쓰는 calloc, malloc, free 함수들이 명시적 힙 동적 바인딩 방식이에요
묵시적 힙 동적 방식 (implicit-heap dynamic)
묵시적 힙 동적 바인딩 방식은 java 와 같이 모든 객체들을 Heap 에 할당하는 언어들을 말해요
스코프(Scope)
스코프는 변수가 쓰일 수 있는 범위를 말해요
nonlocal variable : 프로그램 유닛이나 블럭(중괄호를 생각하면 됩니다) 내에서 사용은 되나 그곳에서 선언되진 않은 변수
정적 스코핑(Static scoping)
변수가 참조될 때 정적으로 해당 변수를 찾아가는 방식을 말합니다.
사실 이 말만 들으면 이해가 잘 안되는데요. 동적 스코핑이랑 비교를 해봐야 이해가 잘 된답니다.
우선 우리가 흔히 쓰는 C언어는 정적 스코핑 방식을 써요
int x = 10; void findB() { printf("%d", x); // 정적 바인딩 : 10, 동적 바인딩 : 20 } void findA() { int x = 20; findB(); } int main() { findA(); return 0; }
변수 x를 참조한다! 라고하면 findB() 에서 x 를 찾아보고 없으면 부모 괄호, 즉 코드상의 부모를 찾아가서 x 를 찾아보죠. 여기서는 전역변수 x 가 되겠네요
그래서 출력하게 되면 10이 출력될거에요
동적 스코핑(Dynamic scoping)
반면 동적 스코핑 언어를 따르는 언어는
int x = 10; void findB() { printf("%d", x); // 정적 바인딩 : 10, 동적 바인딩 : 20 } void findA() { int x = 20; findB(); } int main() { findA(); return 0; }
변수 x를 참조한다! 라고하면 findB() 에서 x 를 찾아보고 없으면 부모
괄호스택, 즉 스택상의 부모를 찾아가서 x 를 찾아보죠. 여기서는 findA의 x 가 되겠네요그래서 출력하게 되면 20이 출력될거에요.
동적 스코핑의 경우 재귀를 주로 이루는 프로그래밍 언어에서 많이 채택하는 방식이에요. 부모함수의 성격? Context 를 잘 이용할 수 있다는 점이 장점이 되겠습니다.
다만 단점이라고하면 프로그래밍이 어려워질거에요.
프로그램이 RUN 되어야 비로소 그 값을 유추할 수 있기 때문이죠.
부모 함수가 무엇이고, 그 호출 순서가 어땠는지에 따라 같은 자식이여도 변수 값이 다를 수 있다는 점이 어려운거에요.
+ 현재의 Local 변수의 값을 특별한 조건문을 통해서 보호하지 않으면 값이 훼손된다는 점
+ 정적인 타입 체킹이 불가능함
스코핑 숨김 현상 (Hidden scoping)
동적 스코핑이든, 정적 스코핑이든 부모 스코프에 있던 변수와 동일한 이름의 변수를 자식 스코프에 만들어주게되면 자식 스코프 내에서는 부모 스코프의 변수에 접근할 수 없게 되는 문제가 생겨요. 이런 현상을 Hidden scoping 이라고 불러요
C에서의 Scope
블럭 스코프 (Block scope)
중괄호를 떠나면 없어지는 변수, 구조체, union 등을 말합니다.
Top-level identifiers 전역 변수나 전역 구조체들도 프로그램 전체라는 block 으로 보기에 여기에 속합니다.
+ Block identifiers = 지역 변수
블럭이라고 함은 이름 스코핑, 저장 공간 할당의 단위인데 중괄호라고 생각하시면 돼요.
한 가지 주의할 점만 보여드릴게요
void func() { int x; { int y; } }
이런 코드가 있을 때 x와 y는 모두 block scoping 이기에 x 는 func() 전체, y는 중괄호 범위까지만 사용이 가능해요
하지만 블럭 = 저장 공간의 할당 단위이기도 해서 func() 가 호출되고 x 가 할당된 다음에 블럭이 실행되면서 y가 할당되는게 아니라
func() 가 호출되면 할당 하는 김에 y까지 다 미리 할당해버리고, 스코프만 달리 적용한답니다.
매개변수 스코프 (Parameter scope)
함수 파라미터에 있는 변수들은 그 함수 body 의 끝까지 스코프를 유지합니다.
파라미터들은 엄연히 말하면 중괄호 밖에서 선언된 변수들이기에 블럭 스코프가 아닙니다.
원형 스코프 (Prototype scope)
함수 원형에서 많이 쓰이는데요. 원형 스코프는 함수 원형이 끝날때까지 그 스코프를 유지합니다.
그래서 아래와 같은 구문이 있을 때 b 에는 10이라는 값이 들어가게 됩니다. (C++ 문법이긴 하지만요)
int add(int a = 10, int b = a);
함수 스코프 (Function scope)
goto 문을 요즘은 거의 안써서 이해가 잘 안되었지만
int main() { int a; // block scope mylabel: // function scope goto mylabel; return 0; }
이런 구문이다.
파일 스코프 (File scope)
함수들은 링크만 되어 있으면 어느 파일이든 호출이 가능합니다. 변수의 경우에는 다른 파일에 있으면 extern 키워드를 붙여줘야하기 때문입니다.
따라서 함수 이름은 파일 스코프를 따른다고 말합니다.
생명주기(Lifetime)
변수의 생명주기는 스코프와 거의 일치하지만 static 변수는 좀 달라요
static 변수는 기본적으로 block scoping 이기 때문에 지역변수로서, 함수가 끝나게 되면 접근할 수 없어요
하지만 생명주기가 전역변수화되어 함수가 끝난다고 변수가 할당해제되지는 않는다는 점이 특징이에요
참조 환경 (Referencing Environment)
프로그램 내에서 참조할 수 있는 변수들 그 이름들을 symbol 이라고하는데 프로그램이 실행되기 전에 symbol 들을 모아서 어떤 심볼이 있는지 확인하는 작업이 있어요. 이를 심볼 테이블을 구축한다 라고 말하는데
먼저 함수안에 함수를 만들 수 있는 C를 가정해볼게요
void func1() { int a; void func2() { int b; } } int main() { int c; func1(); return 0; }
이런경우 심볼테이블은 다음과 같이 만들어질거에요
함수가 실행되면서 심볼 테이블에 심볼이 추가되기도, 삭제되기도 하는데 func2 까지 모두 실행된 경우를 그려봤어요
어떤 변수가 참조되려고 할 때 심볼 테이블 상에서 level 이 높은 순으로 탐색되면서 변수를 찾아가겠죠?
심볼 테이블에 대해 자세히 다루는 시간이 아니기 때문에 이정도까지만 설명하고 넘어갈게요
Named Constant
기본적으로 C언어는 상수에 이름을 붙이는 행위가 union 만 가능해요
#define N 100
은 전처리 구문으로, CC 혹은 CPP(C Pre Processor) 를 통해 온전한 C 프로그램으로 바뀌게 됩니다.
그럼 온전한 C 프로그램에서도
const int result = 100;
이런 구문이 가능한데 이 또한 상수로 취급하는 것이 아니라 값을 변경할 수 없는 변수로 취급한다고 해요. ( 말이 좀 웃기죠? 변하지 않는 변수라니.. )
enum NUM { ZERO, ONE, TWO ... };
이런 구문을 보면 0의 별칭으로 ZERO, 1의 별칭으로 ONE 을 붙여준거에요.
오직 enum 만이 순수한 이름 붙여진 상수이다. 라는거 기억해두시면 좋을거같아요.
변수 초기화
int a = 1; // 초기화 O int main() { int b = 1; // 초기화 X, 변수 선언 후 1이라는 값 할당에 불과함 return 0; }
변수 초기화의 경우에도 전역 변수와 지역변수의 관점이 다른데요
전역 변수에 int a = 1; 이라는 구문은 정말 생성과 동시에 값을 초기화하는 관점이라면
지역 변수인 int b = 1; 이라는 구문은 b 를 만듦과 동시에 1이라는 값을 할당해주는 과정을 생략하여 쓴것이라고 해요
'프로그래밍언어' 카테고리의 다른 글
구문 분석 및 파싱 (1) 2020.05.03 데이터 타입 (1) (0) 2020.05.01 Lexical And Syntax Analysis (0) 2020.04.30 프로그래밍 언어 Semantic 표현 (0) 2020.04.29 프로그래밍 언어 Syntax 표현(기술) (0) 2020.04.27