힘들게 공룡과 기사가 그려진 컴파일러란 책을 보고 깨달은 것이다. 책은 정말 두꺼운데 내용을 요약하면 10% 정도로 줄겠더라. 정말 읽기 피곤할 정도로 난잡하게 쓴 책이다.
어셈블리어란?
기계어란 것은 컴퓨터가 이해하는 명령어로 숫자로 되어 있다. 이 숫자를 사람이 읽기 피곤하기 때문에 단어로 바꾼 것이 어셈블리어이다. 예를 들면 다음과 같은 꼴이다. 어떤 CPU의 명령어라고 하자.
9797 1234 5678 = Add $1234, $5678 = Acc ← $1234 + $5678
9797(꾸질꾸질) = Add = 더하라, $는 수가 아니라 주소를 의미. 1234, 5678은 메모리 주소이다. 내용은 1234번지와 5678번지 내용을 더해서 Acc 레지스터에 넣어라 하는 더하기 명령이다.
반복문과 조건문은 어셈블리어에서 보면 본질적으로 같다. 조건 판단(크다/작다/같다/다르다)을 하는 부분과 거기에 따라 실행할 명령어가 담긴 메모리 주소로 이동(점프)하는 부분으로 되어 있다. 그래서 어셈블리어에서도 조건문과 반복문은 고급언어처럼 보이게 할 수 있다. 반복문과 조건문 번역은 그렇게 어렵지 않다.
고급 언어와 어셈블리어 언어의 차이는 아마도 수식을 번역할 수 있느냐 없느냐 차이 정도? 정말 단순한 어셈블리어는 단어 vs 숫자 번역기 같은데 복잡한 어셈블리어는 수식도 번역해 준다. (인문계 친구에게 수학도 언어라고 얘기했더니 노발대발 하던데... 컴퓨터 분야에선 수학도 언어의 일종이라 언어 처리 기술을 사용한다. 유명한 과학자가 한 말이 있는데 수학은 우주를 표현하는 언어라고 하더라.)
인터프리터란?
고급언어에서 문제는 반복문, 조건문이 아니라 사람이 쓰는 수식을 기계가 이해하기 쉬운 순서로 바꾸는 것이다. 예를 들면 아래와 같다.
1234 + 5678 ⇒ + 1234 5678 ⇒ Add 1234, 5678 ⇒ Add(1234, 5678)
인간의 표현에선 연산자가 중간에 온다. 이걸 제일 앞에 나오게 하는 접두사, 제일 뒤에 나오게 하는 접미사 표현으로 바꾼다. 접두사 표현은 어셈블리어와 비슷한 영어식 어순이고, 함수 표현과도 같다. 접미사 표현은 한국어 어순과 같은 것이다.
1234 + 5678 ⇒ 1234 5678 + ⇒ 1234와 5678을 더해라
이렇게 수식을 번역하고 바로 실행하는 것을 인터프리터라 한다. 고로 매번 실행할 때 번역 시간이 소모된다. 이 소모 시간을 피하려면 번역한 내용을 지우지 말고 재활용해야 하는데 메모리가 그만큼 소모된다.
반복문과 조건문 번역은 수식 번역만큼 오랜 시간이 걸리지 않는다. 고로 수식 번역 결과 정도만 따로 보관해도 처리 속도는 향상될 것이다. 아니면 반복문과 조건문까지 포함해서 번역한 것을 일정 단위로 저장했다가 재활용하는 것이다.
인터프리터의 장점은 메모리 절약 정도? 쉽게 코드를 수정할 수 있다는 것? 프로그램을 조립해서 사용하기 쉽다는 정도? 느리다는 것을 빼면 다 장점이다.
※ Java(자바)는 가상 머신 위에서 동작하는 인터프리터 언어이다. 그러니까 가상 머신의 코드를 진짜 머신의 코드로 바꾸는 과정이 필요하다. 그래서 실행 속도가 느리다. 그 경쟁자 C#은 가상 머신 코드를 진짜 실행 코드로 컴파일 하는 것도 추가 한다.
컴파일러란?
번역한 내용을 따로 만들어 두고 그것을 실행하기 때문에 번역 시간은 1회만 소모되고 처리 속도가 빠르다. 대신 번역한 내용을 담아야 하는 메모리 소모, 코드를 일부 수정하면 전체를 다시 번역해서 다른 부분들과 조립해야 하는 번거로움이 있다. 실행 속도가 빠르다는 것을 빼면 다 단점이다. 허나 인터프리터 요소를 살짝 섞은 조립식 컴파일러가 나온다면 얘기는 달라진다. 예를 들어 모든 객체/함수를 별도로 컴파일 해 두는 것이다. 실행할 때 즉석에서 조립하여 연결하는 방법을 쓴다.
이런 경우 세상 모든 함수, 모듈, 객체에 대한 이름 구분이 필요해진다. 이놈이 만든 함수와 저놈이 만든 함수가 이름이 같으면 어느 것을 붙여야 하겠는가? 경상도 홍길동인가 전라도 홍길동인가? 보통 간단하게 같은 폴더 안에 있는 지역 함수와 공통으로 사용하는 폴더의 전역 함수만 조립하면 된다. 이렇게 할 경우 그 함수만 수정해서 컴파일 한 후 넣으면 유지보수는 간단히 끝난다.
예를 들어 다음과 같은 코드가 있다고 하자.
A = B + C
D = A * E
여기서 A, B, C, D, E 변수의 주소는 가변적일 수 있다. 프로그램을 실행시킬 때 메모리 위치를 어디로 잡느냐에 따라 실제 주소는 변할 수도 있다. 그렇다면 메모리에 읽어 올 때 변수의 주소를 결정해서 번역해 주는 과정이 필요하다. 그래서 이런 번잡한 과정을 피하고자 메모리를 블록/세그먼트/페이지/클러스터/섹터 등 (이름이 참 많다) 구간으로 나누는 방법을 쓴다.
$1 = $2 + $3
$4 = $1 + $5
이렇게 번역한 후에 블록/세그먼트/페이지/클러스터/섹터 뭐라 하든지 그것의 시작 주소만 결정해 주면 다음과 같이 자동으로 계산 처리 된다.
시작 주소 = S
$(S+1) = $(S+2) + $(S+3)
$(S+4) = $(S+1) + $(S+5)
이런 식의 실행 전에 바로 조립을 하려면 O/S의 도움 없인 불가능하다. O/S가 모든 주소를 번역하거나 시작 주소를 결정해 주어야 한다. 마찬가지로 조립할 함수들의 위치를 결정하면서 함수들의 주소도 번역해 주어야 한다. 이 과정이 거의 인터프리터가 하는 작업과 비슷한 수준이라 적절히 섞자는 것이다. 모든 주소를 번역하기는 힘들고 시작 주소만 결정해 주는 것이 좋은데 그건 O/S보다 CPU의 기능이 가능해야 한다.
※ 아마도 이와 비슷한 개념이 윈도우즈의 DLL 같은 것, 자바의 클래스 파일 같은 것이겠다. 이 뒤를 잇는 C#에서도 역시 클래스 명칭 구분을 위한 방법들이 나온다. (IT 분야엔 고수가 적고 하수가 많다. 이 명칭 구분에 대한 이해도 떨어지는 사람들이 많더라. 학원 강사라면서 말이다. 어떻게 초보자보다 못 하냐?)
단어 인식 ↔ 수식 인식 ↔ 문장 인식
예를 들어 조건문, 반복문, 선언문, 대입문은 문장이라고 한다. 그 속에 들어가는 것이 수식이다. 수식 안에 단어들이 들어 있다. 고로 단어 인식기가 가장 하위직이고 중간이 수식 인식기이며 최고 상급자가 문장 인식기가 된다.
- 선언문이란 단어(변수/상수/함수)의 품사(문법 특징)를 정해주는 것과 같은 것이다.
- 대입문이란 수식의 계산 결과를 단어(변수/함수)에 넣어주는 것이다.
- 조건문이란 수식의 계산 결과를 보고 무엇을 실행할지 (어디로 갈지) 결정하는 것이다.
- 반복문이란 수식의 계산 결과를 보고 반복할 것인지 결정하는 것이다.
수식은 우리가 아는 수학에서 수식과 같은 것이다. 그 수식 안에는 단어들이 들어 있다. 단어는 변수, 상수, 함수이다. 이것들은 공백, 기호(소수점/괄호), 연산자로 구분할 수 있다. 단어는 알파벳, 숫자, 기호, 한글, 한자 등 문자 차이를 보고 구분이 가능하다.
- 상수 : 값이 변하지 않는다. 예) π = 3.141592, 긴 숫자보단 문자 π를 쓰면 편하지?
- 변수 : 값이 변한다. 값이 미정이다. 열어 봐야 안다. 계산해 봐야 안다.
- 함수 : 파라미터(매개변수)에 따라 정해진 계산 결과 값이다.
사람이 정해주는 단어에 해당하는 변수, 상수, 함수는 알파벳과 숫자의 조합이다. 함수와 변수의 차이는 함수 뒤에는 괄호가 온다는 것이다. 수학 상수 π나 e와 같은 것은 변수처럼 이름을 붙일 수 있다. 이러면 변수인지 상수인지는 단어만 보곤 알 수 없게 된다. 고로 단어장이 필요해진다. 숫자로 된 상수는 다음과 같은 패턴이다.
-123.456e-789
부호/정수.소수e부호/지수
정수/소수/지수 = 0~9의 반복
이 패턴을 보고 이 게 한 단어라는 것을 판독하는 게 단어 인식기다. 단어 인식기에는 괄호 구조 인식 기능이 없다. 수식/문장 인식기의 경우는 괄호 구조를 인식해야 한다. 즉 열었으면 닫아야 하는 구조로 되어 있다. 물론 원한다면 단어 인식기에도 괄호 구조를 넣을 수 있다. 단어의 시작(접두사)과 끝(접미사)이 어떤 궁합을 가지고 있다면 말이다. 헌데 그런 단어는 세상에 없으니 만들 필요 없다. 필요한 것이 있다면 변수, 상수, 함수를 구분해 주는 접두사 정도는 있으면 편하다.
문장 인식기는 수식이 들어갈 자리가 나오면 수식 인식기를 호출하여 수식 부분 번역을 담당하게 하거나 계산 결과를 가져오라고 한다. 수식 인식기는 단어가 나오는 부분에선 단어 인식기를 호출해서 그 단어가 어떤 품사인지 내용물은 뭔지 가져오라고 한다. 단어 인식기는 문자 하나씩 읽으면서 이질적인 문자가 나오는지 확인하여 단어를 구분해 내고, 단어장에서 검색해 본다.
단어 인식기에 많이 사용하는 방법은 상태 변화도, 상태 변화표이다. 그림으로 그리면 상태 변화도이고 이걸 표로 만들면 상태 변화표이다. 예를 들어 위의 수식 패턴에서 부호나 기호는 한 문자라서 간단한데 정수, 소수, 지수는 0~9의 반복이다. 이렇게 같은 종류의 문자가 계속 나오면 그 구간은 같은 상태로 보는 것이다.
현재 상태 + 입력 문자 → 필요 행동 + 다음 상태
if 현재 상태 and 입력 문자 then 뭔가 하고 다음 상태로 변경 end
예를 들어 정수부를 읽는 상태라면 입력 문자가 계속 0~9가 나오면 계속 정수 상태를 유지하다가 소수점이 나타나면 정수 상태는 끝나는 것이다. 나머지 소수부, 지수부도 정수부와 같기 때문에 상태만 다르지 처리 방법은 같은 식이다. (난 지금 숫자 상수를 읽고 있어, 그 중에서 정수부를 읽고 있어. 다른 기호가 나올 때까지 말이야.)
이렇게 읽은 10진수를 2진수로 바꾸려면, 일단 정수부와 소수부를 붙여 정수로 보고 2진수로 바꾼다. 여기에 부호를 적용하고, 소수점 위치만큼 10을 곱하거나 나누기 한다. 지수부도 읽은 후에 부호를 붙여 2진 정수로 바꾼 후 그 만큼 10 곱하기/나누기를 한다. 그럼 2진수 실수로 바뀌어 저장이 된다.
※ 실수의 경우 2진수는 10진수로 정확히 바뀌는데 10진수는 2진수로 정확히 바뀌지 않는다. (이상한데? 모든 수는 2로 정확히 나눌 수 있어 정확하게 바뀌는데? 메모리 한계 때문에 정밀도가 떨어진다는 의미이다.) 10진수 문자열(표현형식)과 2진수 실수(저장형식) 사이 변환 과정에서 2 곱하기 나누기를 반복하는 것보다는 한 번에 10의 거듭제곱이나 2의 거듭제곱을 곱하고 나누는 게 더 빠르다.
문법 표현
단어의 구조를 표현하는 문법이 정규 문법, 정규식이라고 한다. 수식이나 명령문을 표현하는 문법이 문맥 자유 문법이다.
- 생성 문법 : 추상 표현(비단말) → 구체 표현 대체(단말 포함)
- 정규 문법 = 정규 표현식(정규식) : 비말단 → 말단/{말단+비말단}/{비말단+말단}/생략
- 문맥 자유 문법 : 비말단 → 말단&비말단 조합
- 문맥 의존 문법 = 문맥 민감 문법 : 앞+{비말단}+뒤 → 앞+{말단&비말단 조합}+뒤
- 재귀 열거 문법 : 재귀 열거 언어(재귀 열거 집합)를 표한다고 하는데 알 거 없다.
※ 말단/단말 : 구체적 단어/문자/기호, 비말단/비단말 : 추상적 계층(식/항/구/절)
좌측의 추상적 표현을 우측의 좀 더 구체적 표현으로 대체하면서 문장을 만들어가는 작문 문법이 생성 문법이다. 재귀 열거 문법이 뭔지 모르겠으나 형태를 단순하게 제약한 것이 문맥 의존 문법이다. 그러니까 이건 앞과 뒤를 보고 중간을 대체하여 작문을 하는 것이다. 앞 뒤 문맥을 본다는 말이다. 앞과 뒤가 안 맞으면 대체 불가능하다.
문맥 의존 문법 중에 형태를 단순하게 제약한 것이 문맥 자유 문법이다. 앞뒤 상황을 보지 않고 그냥 기계적으로 대체하면 된다. 컴퓨터 언어(수식, 명령문)는 모두 이 문법에 속한다. 이 문법으로 괄호 구조를 표현할 수 있다. 문맥 자유 문법 중에서 형태를 단순하게 제약한 것이 정규 문법이다. 비말단은 반드시 말단을 포함하며 비말단은 하나인 단순한 형태로 대체 된다. 이 문법은 괄호 구조를 표현하지 못 한다. 단어 구조 표현은 이 문법에 속한다. 이 문법을 수식처럼 표현한 것이 정규식이다.
이렇게 문법으로 표현해도 번역기를 만들 때는 직접적인 도움이 안 된다. 그 문법이란 것은 작문(생성)을 할 때 도움이 되는 문법이지 역으로 독해(해석)를 할 때는 도움이 안 된다. 독해할 때는 거꾸로 된 문법이 필요하다. 예를 들면 다음과 같은 식이다. 생성 문법을 거꾸로 읽어 적용해야 한다.
해석 문법 : 구체 표현 → 추상 표현 대체
독해를 거쳐 어떤 순서(어순)로 나열된 1차원적 배치의 문장(영어)이 순서가 없는 입체적 연결 상태(문장 성분 사이의 관계도로 나무 가지 모양)로 바뀐다. 이 연결 상태를 어떤 문법에 따라 어떤 순서로 나열된 문장(한국어)으로 바꾸는 것이 번역이다. 단순 단어 대체가 아닌 어순을 바꿀 때는 먼저 독해를 완벽하게 끝내야 한다. 일단 독해가 끝나는 시점에 이 수식/문장이 문법에 맞는지 틀린지 결판이 난다.
I am a boy → I=(a+boy) → 나=(한+소년) → 나+이다+(한+소년) → 나는 한 소년이다.
생성 문법을 어떻게 표현하든 결국 해석 문법을 보고 상태 변화도, 상태 변화표를 만들어야 구현할 수 있다. 여기서 상태는 추상적 계층이 된다. 입력은 읽은 문자/단어가 된다. 헌데 수식의 경우처럼 뒤의 연산자까지 읽어 봐야 결정이 되는 경우는 입력 값에 다음 연산자도 포함이 된다. 즉 다음 문자/단어를 읽어 봐야 어떤 결론이 나는 경우는 상태 변화도에서 다음 문자/단어도 입력으로 반영 되어야 한다. 고로 상태 변화도가 상당히 복잡해진다. 생성 문법을 보고 다음에 나올 수 있는 가능한 모든 문자/단어를 찾아야 하는 문제가 있다.
- A + B * C → A + (B * C) : B 뒤에 곱하기가 있어 이렇게 번역 된다.
- A * (B + C) → A * (B + C) : B 앞에 괄호가 나오기 때문에 이렇게 번역 된다.
- A * B + C → (A * B) + C : B 뒤에 더하기가 있어 이렇게 번역 된다.
고급언어의 수식 번역과 계산
다음과 같은 접간사 형태의 수식을 접두사나 접미사 형태로 바꾸는 것을 말한다.
원문 : A = B + C * (D + E)
번역 : A ← + B * C + D E or A ← D E + C * B +
번역 : A ← 합(B, 곱(C, 합(D, E))) or A ← (((D, E)합, C)곱, B)합
영어식 어순으로 번역했을 경우 읽는 순서 그대로 계산하기 힘들다. 이건 복합문을 단문으로 나누어 계산 순서에 맞게 배치해야 계산이 가능하다. 예를 들면 아래와 같다.
순서1 : Acc ← 합(D, E)
순서2 : Acc ← 곱(C, Acc)
순서3 : Acc ← 합(B, Acc)
순서4 : A ← Acc
그런데 역순으로 읽었더니, 다시 말해서 한국식 어순으로 번역했을 경우 읽는 순서 그대로 계산하기 쉽다. 예를 들면 아래와 같다.
0단계 : (((D, E)합, C)곱, B)합 = D, E, 합, C, 곱, B, 합
1단계 : Acc ← D, E, 합
2단계 : Acc ← Acc, C, 곱
3단계 : Acc ← Acc, B, 합
4단계 : A ← Acc
그러니까 어떻게 하든 원문을 이 순서로 번역만 하면 되는 것이다. 방법은 2가지가 있는데 책에서 설명하는 아주 복잡한 방법으로 수식을 1회만 쭉 읽으면서 번역하는 방법이 있고, 다른 하나는 사람이 계산하는 방법처럼 여러 번 읽으면서 다시 쓰기 하는 방법이다. 일단 사람이 하는 방법부터 보자.
단순 무식해서 이해하기 쉬운 다시 쓰기 방법
0단계 : A = B + C * (D + E)
1단계 : A = B + C * T1 ☞ T1 = D + E
2단계 : A = B + T2 ☞ T2 = T1 * C
3단계 : A = T3 ☞ T3 = T2 + B
번역 결과 :
T1 = D + E
T2 = T1 * C
T3 = T2 + B
A = T3
원문에서 계산 순서가 높은 것을 찾아서 그것부터 번역하고 변수 하나로 대체해 가는 방식이다. 이 때 수식을 다른 곳에 다시 써야 한다. 이 방식으로 하면 메모리는 원문의 3배 정도만 필요하다. 번역문이 들어갈 공간 C, 다시 쓰기 할 공간 A, B이다. 다시 쓰기 할 때는 A와 B 사이를 왔다 갔다 하면 된다. 다시 쓰기 하는 시간 낭비는 있지만 단순 명확해서 오류가 나기 힘들다. 또한 한 번만 번역하면 계속 재활용할 수 있다.
고급스럽고 복잡해서 이해하기 어려운 함수 재귀 호출
0단계 : A = B + C * (D + E)
1단계 : A = B +, 여기까지 읽고 A = 는 마지막 처리 위해 기억
2단계 : A = B + C *, 여기까지 읽고 B + 도 나중 처리 위해 기억
3단계 : A = B + C * (, 여기까지 읽고 C * 도 나중에 처리하기 위해 기억
4단계 : A = B + C * (D + E), 여기까지 읽고 T1 = D E + 로 번역하고 상위로
5단계 : A = B + C * T1, 보류했던 것을 T2 = C T1 *로 번역하고 상위로
6단계 : A = B + T2, 보류했던 것을 T3 = B T2 +로 번역하고 상위로
여기 방법은 함수 재귀호출을 이용해 보류했던 내용을 스택에 보관하는 방법이다. 이 방법은 접미사 형태의 번역문을 스택에 보관한다. 이 때 변수 대신 변수의 값을 직접 넣어 두면, 스택에서 빼 오는 순서 그대로 계산하면 된다. 앞의 방법과 번역문의 메모리(스택) 사용 양은 같다. 계산 속도도 같다. 앞의 방법은 번역된 것을 계속 가지고 있고, 이 방법은 계산하는 과정에서 스택의 내용이 변하기 때문에 다신 번역(어순 변경 작업)해야 한다.
1항 연산자, 2항 연산자, 우선순위와 미리 읽기
우리가 아는 가감승제 4칙 연산은 2항 연산자다. 3항 연산자는 없다. 연산자가 좌우 양쪽의 값으로 계산을 한다. 헌데 음수 기호는 1항 연산자다 우측의 부호만 바꾼다. 문제는 부호 바꿈 기호가 빼기 연산자와 같은 모양이란 것이다. (차라리 음수 부호는 다른 걸 쓰는 게 더 간단하다.) 이 경우 연산자 2개가 겹쳐 나오게 되는 것이 힌트다. 괄호 기호도 포함해서 연산자와 괄호가 겹쳐 나오면 궁디(뒤)부터 처리하란 신호다.
원문 : A = B * -C + D * (E + F)
번역 : C~, B *, E F +, D *, +, A =.
해석 : C를 음수로, B와 곱하고, E와 F 더하고, D 곱하고, 앞 2개 결과 더하고, A에 대입하라.
빼기 연산자와 부호 바꾸기 연산자를 구분하기 위해서 ~로 고쳤다. 여기서 읽다 보면 *와 –가 이어 나오고, *와 (가 이어 나온다. 그런 경우 먼저 뒤의 것을 먼저 처리해야 한다. 수학에선 *와 /와 괄호로 묶인 것을 항이라고 한다. 항은 먼저 계산해야 한다. 전통적 수식이 계산 순서대로 배치되어 있지 않기 때문이다. 또한 우선순위가 낮은 +와 – 연산자의 경우 한 단계 더 읽어서 뒤에 우선순위가 높은 *와 /가 나오는지 확인해야 한다. 이를 미리 읽기라 한다.
※ C언어에선 대입 연산자 =를 2항 연산자로 취급한다. 복잡한 수식 안에서 여러 변수에 같은 값을 대입할 수 있다는 장점은 있지만 쓸데없이 복잡하기만 하다. C언어는 좀 허세가 심하다. 목표에 충실한 가장 간단한 방법을 가장 많이 사용한다. 결국 이런 문법을 복잡한 수식 안에서 사용 안 하는 이유는 소스 코드 독해가 어렵기 때문이지. 그래서 오직 초기화 할 때만 사용한다. A = B = C = D = 3.141592.
선언문 = 단어장
C언어를 보면 제일 앞에 변수와 함수 선언문이 나온다. 함수의 경우 내용 정의가 없지만 문법 확인을 할 수 있는 정도의 정보는 나열되어 있다.
결과 값 리턴 타입, 함수이름, 매개변수(파라미터) 개수와 종류
이렇게 하는 이유는 아래 앞으로 나올 코드들의 문법 확인을 쉽게 하기 위해서다. 미리 단어장을 만들어 두는 것이다. 이렇게 하지 않으면 새로운 변수, 함수를 만날 때마다 단어장에 넣어야 하며, 지금 보고 있는 문장 문법 확인하기 위해서 뒤의 함수 정의 등을 찾아 볼 수는 없기 때문이다. 사람이나 기계나 단어 정의는 먼저 해 두는 것이 편하다.
- 다음 나열된 단어는 상수이다. 그 불변 값은 이렇다.
- 다음 나열된 단어는 변수이다. 그 초기 값은 이렇다.
- 다음 나열된 단어는 함수이다. 매개변수는 이렇다.
단어의 종류, 즉 품사 구분을 해 주는 접두사나 접미사를 단어에 붙여 표현하는 것이 좋다. 왜? 컴퓨터나 사람이나 그 단어를 단어장에서 찾아보지 않고도 문법이나 문맥 검사를 해 볼 수 있기 때문이다. 옛날 Basic 언어에는 이런 접미사가 있다. 프로그래머가 일부러 만들어 붙일 수도 있다. 이런 기능은 코드 쓰기엔 불편하지만 나중에 읽을 때 편하다. 그래서 편집기에서 자동으로 붙여 주는 것이 좋다.
- 상_원주율 = 3.141592 ☞ 고정 불변
- 변_반지름 = 100 ☞ 초기 값으로 100을 지정
- 함_원둘레(반지름) ☞ 반지름만 알면 된다.
- 함_원면적(반지름) ☞ 반지름만 알면 된다.
※ Apple Mac 컴퓨터에서 사용하는 Objective C라는 언어는 함수 파라미터에도 설명문을 붙일 수 있더라. 그래서 함수처럼 보이지 않던데 코드에 대한 설명을 붙이라고 강요하는 것 같더라.
간단한 형식 문법 체크, 예측 문법
상세한 번역 전에 간단한 형식적 문법 확인부터 먼저 하는 것도 좋다. 전반적인 균형과 틀을 보는 것이다. 이런 형식적 문법 체크 기능은 편집기에 넣으면 좋다. 즉 한 줄 입력하면 바로 문법 확인해서 고치도록 알려 주는 것이 좋다.
- 단어 철자가 틀렸는지 : 특히 I(아이)와 l(엘)과 1(일), 0(영)과 O(오우) 등
- 괄호의 짝이 맞는지 : {[(~)]} “~”, ‘~’열린 것은 닫혀야 한다.
- 구분자가 맞는지 : .,:;!? 앞과 뒤를 구분하며 끝과 시작을 알린다.
- 파라미터 개수 : 함수는 파라미터(매개변수) 개수가 정해져 있다.
- 연산자 중첩 : 기호 2개를 붙여 하나의 연산자로 취급할 수 있다.
- 반복/조건문의 틀 : 괄호의 짝을 맞추는 것과 비슷한 것이다.
이런 문법 확인이 가능한 이유는 예측 문법 때문이다. 앞부분의 형태를 보면 뒤에 어떤 틀이 나올 것인지 예측이 가능하기 때문이다. 고로 그 틀만 맞는지 확인하면 된다.
하나 의아한 것이 있는데 왜 편집기에서 반복문 조건문의 틀을 잡아 주지 않는지 모르겠다. 반복문과 조건문은 괄호와 비슷해서 자기 짝이 있다. 번역할 때는 자기 짝을 확인하기 위해서 눈에 보이지 않지만 키워드에 ID를 붙여야 한다. 프로그래머는 일부러 설명문 형태로 눈에 보이게 ID를 붙이기도 한다.
HTML언어에선 괄호에 ID를 붙이는 방식이다. 시작 괄호엔 /가 없고 끝 괄호엔 /를 넣는다. 괄호의 ID는 <~> 사이에 넣는다. 고로 괄호 <~>는 함부로 사용할 수 없는 기호다.
<이름> 여기엔 보통 단어나 문장이 들어간다. </이름>
이렇게 하는 이유는 중첩 괄호가 무지 많이 나오기 때문에 자기 짝을 확인해야 할 필요가 있기 때문이다. 그걸 프로그래머가 하라고 이렇게 만든 것인데 컴퓨터가 확인해 주는 것이 더 바람직하다. 이런 기초적인 문법은 실수하지 않도록 컴퓨터가 틀로 제공하고 사람이 못 건드리도록 하는 것이 좋다.
위보다는 <이름: 어쩌구 저쩌구 :이름> 형태가 더 깔끔하지 않나?
반복/조건문에선 조건문 쪽에 애매모호한 문제가 있다. 컴퓨터 언어에선 괄호처럼 짝이 맞거나 구분자처럼 끝과 시작을 확실하게 해 줘야 한다.
if ~ then ~
if ~ then ~ else ~
if ~ then if then ~ else ~ = if ~ then (if then ~) else ~ = if ~ then (if then ~ else ~ )
위와 같은 형태는 종료를 알려 주는 키워드가 없다. 고로 중첩해서 사용할 경우 애매모호한 해석에 빠진다. 즉 이렇게 해석할 수도 있고 저렇게 해석할 수도 있는 경우가 나온다. 그래서 그걸 해결하는 복잡한 번역기를 만들기보다는 다음과 같은 형태로 문법을 바로 잡는 것이 더 간단하다.
if ~ then ~ 끝
if ~ then ~ else ~ 끝
if ~ then (if ~ then ~ 끝) else ~ 끝 ≠ if ~ then (if ~ then ~ else ~ 끝) 끝
※ C언어에선 이 문제를 해결하기 위해 문장 블록을 괄호 {~}로 표현하고, 문장 구분을 ;으로 하고 있다. 즉 문장의 끝과 시작을 알리는 것이 ;이다. 여러 문장들을 묶을 때 {~}로 감싼다. C언어는 수식처럼 괄호의 중첩이다. 그래서 C언어를 배울 때 마치 수식을 보는 듯한 착각을 일으킨다. C언어가 고급 언어가 아니라고 하는 이유 중에 하나이다. 명령문이 마치 수식처럼 보인다. 솔직히 다른 고급 언어 문법에 C언어가 가진 장점만 섞는 게 더 바람직하겠다.
상수, 변수, 함수, 포인터(간접 주소), 배열과 문자열
- 상수란 항상 일정한 수를 말하기 때문에 프로그램 시작과 끝까지 값이 변하지 않는다.
- 변수란 변하는 수를 말하기 때문에 값을 바꿀 수 있다. 고로 변수는 값을 넣을 메모리 공간의 주소로 표현 된다.
- 함수란 수를 처리하는 상자란 의미다. 고로 매개 변수를 받아 계산한 후에 결과 값을 주는 코드의 주소로 표현 된다. 매개변수와 결과 값은 스택을 통해 전달한다.
- 문자열이란 문자들의 1차원 나열이다. 즉 단어나 문장이 된다.
- 배열이란 같은 구조의 n차원 나열이다. 예를 들어 99단이라고 하면 2차원 배열이다.
- 포인터란 문자열, 배열, 함수처럼 거대한 내용의 시작 주소이다.
보통 숫자를 다룰 경우는 이 차이를 인식하지 못 하는데 배열과 문자열을 다루게 되면 차이를 느끼게 된다.
- 3.141592...는 상수이다.
- π는 상수를 간단하게 표시한 것이다.
- 수식에서 보통 a, b, c 등 알파벳으로 표기한 것은 변수이다.
- sin, cos, tan, exp, log 등은 함수이다.
- 문자열은 문자의 배열이다.
- 배열은 수식에서 변수 이름 옆에 붙이는 첨자와 같은 개념이다. a₁, a₂, a₃, a₄
보통의 고급 언어에선 문자열 처리가 쉽지만 C 언어에선 문자열 처리가 어셈블리어처럼 불편하다. 여기선 상수 문자열과 변수 문자열의 차이가 명확해진다. 문자열 변수의 경우 길이가 변하는 문제가 있다. 그건 메모리 공간 할당의 문제를 말한다. 고로 보통 문자열은 힙이라는 곳(단어장이나 연습장 정도?)에 저장하고 변수는 이 주소만 담고 있다. 고로 주소만 살짝 바꾸면 문자열을 직접 지울 필요 없이 지운 것처럼 보인다.
주소1: 홍길동
주소2: 을지문덕
주소3: 김구
변수A = 주소2 = 을지문덕
이렇게 문자열을 가리키는 주소를 담고 있는 변수가 문자열 변수인데 문자열만 가리키지 않고 같은 자료 형의 단순 반복인 배열(아파트 구조), 객체(변수+함수)나 다른 변수까지 가리키게 할 수 있지 않을까? 그런 변수가 포인터 변수이고 간접 주소 방식이라고 한다. 그러니까 A(포인터)를 찾아 가니 B(변수)로 가라고 하고, B(변수)를 찾아가니 C(값)를 알려주더란 것이다.
※ 헌데 C언어의 포인터 표기법은 정말 쓸데없이 복잡한 허세다. 그렇게 복잡하게 만들어도 결국 사람들이 쓰지 않잖아? 소스 코드 독해가 너무 어려우니까. 같은 목적지에 도달하는 더 간단한 방법을 쓰지 왜 복잡한 방법을 쓰겠어? Java 같은 언어는 같은 포인터 선언을 더 쉽게 한다.
함수 호출할 때는 스택(먼저 넣으면 나중에 빼게 되는 구조)에 예를 들어 이렇게 내용을 저장한다. 이건 서로 약속이 되어 있어야 한다.
1단계 : 결과 값을 받을 공간 저장(사용자 작성 프로그램에서 하는 짓)
2단계 : 파라미터(매개변수)들 저장(사용자 작성 프로그램에서 하는 짓)
3단계 : CPU/MPU 레지스터 상태 저장 (기계가 자신의 정신 상태를 자동 저장)
4단계 : 복귀할 주소 (기계가 자동으로 저장)
5단계 : 함수 실행 코드가 있는 주소로 점프
항상 돌아올 곳을 알아야 한다. 그래서 점프할 때 기계가 자동 저장을 한다. 호출 당한 함수는 3/4단계 정보는 건드리지 않아야 한다. 자신이 필요한 파라미터의 수와 종류를 알기 때문에 스택에서 읽어낼 수 있다. 계산 결과도 어디에 저장할지 알고 있다. 일을 다 처리한 후에 원상 복귀 명령이 실행되면 3/4단계 정보는 기계가 자동으로 읽어 복구 시킨다. 그럼 호출을 한 쪽에서 2단계 내용을 지우고, 1단계 내용을 받으면 된다.
문맥(같은 표현에 다른 의미) 확인
컴퓨터에서 문맥이라 하면 주로 변수의 종류(Type타입)가 어울리는지 확인하는 것이다. 겉으로 본 형식 문법에는 문제가 없는데 막상 내용을 보니 문자열과 수를 더하라거나(방법은 2가지), 정수와 실수를 더하라거나(방법은 3가지) 하는 문제가 있을 수 있다.
문자열을 숫자로 바꾼 후 더하기 : “123” + 876 = 999
숫자를 문자열로 바꾼 후 붙이기 : “주소” + 123 = “주소123”
정수를 실수로 바꾼 후 더하기 : 123 + 876.999 = 123.000 + 876.999 = 999.999
소수점 이하를 버린 후 더하기 : 123 + 876 = 999
소수점 이하 반올림 후 더하기 : 123 + 877 = 1000
OK 문자열은 이해하겠는데 정수/실수 구분이 있어? 아쉽게도 컴퓨터 진화에선 정수가 먼저 생기고 실수가 나중에 생겼다. 그래서 저장 형식이 서로 다르다. 그러나 정수를 실수로 변환하는 것은 손바닥 뒤집기처럼 쉽다. 정수가 따로 존재하는 이유는 간단한 개수 셀 때 편하기 때문이다. 실수는 공학적 계산을 할 때 필요하다.
정수 : 9876543210
실수 : 9876543210.0123456789
고정 소수점을 취할 경우 정수의 2배 저장 공간을 사용해서 간단하게 실수를 만들 수 있다. 허나 천문학적인 숫자 계산은 할 수가 없기 때문에 부동 소수점 방식을 취한다.
※ 정수, 실수, 바이트, 논리, 문자열 등 기본형 말고 사용자가 조립해서 정의할 수 있는 형이 있다. 이런 것이 발전해서 결국 객체라는 것도 나온다. 여하튼 이런 복잡한 자료 구조를 가리키는 포인터(주소변수)는 당연한 것이 된다.
※ C언어의 포인터 변수 선언은 정말 불필요할 정도로 복잡하다. 참 쓸데없는 짓이다. 자료 구조에 이름을 붙이고 그 이름을 가리키게 하면 되는 간단한 문제인데? 그래서 지금은 포인터 무시하고 Java 이후 객체 지향 언어에선 변수 이름만 가지고 간단하게 쓰고 있지만. 미쳤나? 정력이 남아돌지?
객체 지향 언어란?
문자열은 문자의 배열이었고, 단순 계산기 시절엔 반복 계산한 결과를 배열에 저장했다. 헌데 서로 다른 종류의 데이터 형을 묶을 필요가 생겼다. 그게 사용자 정의 자료 형이다.
컴퓨터가 단순히 계산기 수준을 넘어서고, 문자열을 다루면서 타자기를 대신하게 된 후에, 전쟁 시뮬레이션이나 컴퓨터 게임을 만들게 되면서 객체란 개념이 나타난다. 여기서 객체란 병사, 전차, 전투기, 미사일 같은 유닛을 말한다. 같은 설계로 만들어지지만 각 객체는 서로 다른 위치, 상태에 있다.
- 최대 이동 속도 : 엔진의 힘이라고 보면 된다.
- 최대 이동 거리 : 연료의 양이라고 보면 된다.
- 장갑 두께 vs 무게 : 방어력과 관성에 영향
- 유효 사거리 : 거리에 따라 정확도도 떨어진다.
- 연사 속도 : 분당 발사 수
- 무기 종류 : 총알, 포탄, 미사일
- 현재 위치, 현재 속도(방향 포함), 현재 가속도(방향 포함)
바로 이런 객체 설계도가 사용자 정의 자료 구조였고, 각 객체는 이 자료 구조를 배열로 만들면 되는 거였다. 고로 기존의 고급 언어나 C언어로도 객체 지향 프로그램을 할 수 있다. 객체 지향 언어란 객체 지향 프로그램을 강요한 것을 말한다. 아마 접두사/접미사를 붙이며 프로그래밍을 하던 프로그래머가 언어 문법으로 만들어 강요하게 되었을 것이다.
보통 객체 지향 언어는 특정 자료 구조에 특정 함수들을 연결하도록 강요한다. 함수들은 그 함수가 다루는 자료 구조가 정해져 있다. 헌데 아주 많은 함수들이 이곳저곳에 흩어져 있을 때 그 함수와 자료 구조를 연결하여 해석하기 힘들다. 그래서 서로 짝이 맞는 함수와 자료 구조를 함께 적어 놓도록 강요한 것이 객체 지향 언어이다. 별거 아니지?
이렇게 식구들을 모아 가족을 만들었으면 성을 붙여 줘야 할 거 아닌가? 이 변수들과 저 함수들은 모두 그 객체에 속하니 객체 명칭을 성姓으로 쓰는 것이다. 변수와 함수를 모아서 정의 해야 하고 사용할 때도 성씨를 붙여 줘서 구분해야 하는 것이다. 별거 아니지?
- 전차.속도 = 40km/h
- 전차.위치 = (X, Y, Z)
- 전차.가속도 = 0
- 전차.장갑 = 30mm
- 전차.사거리 = 3km
- 전차.연사속도 = 분당 12발
- 전차.발사체 = 포탄
※ 개체 지향 언어에서 보이는 가장 얼빵한 짓을 예로 든다면 수학 함수, 문자열 함수, 날짜 시간 함수 같은 무소속이 당연한 전역 함수를 객체에 포함시킨 것이다. 이건 객체 지향 목적에도 맞지 않고 불편하기만 하다. 또한 단 하나의 값만 들어가는 기본 자료 형도 객체로 만든 것이다. 객체란 여러 자료 형이 섞여 있는 구조일 때 의미가 있는 것인데 정말 얼빵한 짓이다.
이렇게 하는 이유는 소스 코드 해석 중에 그 자료 구조와 상관있는 함수를 쉽게 파악할 수 있기 때문이다. 그 자료 구조 바로 밑에 함수들을 적어 놓기 때문이다. 객체 지향 언어는 객체 지향 프로그램을 돕는 게 아니라 강요하는 것이다.
객체 지향 프로그램을 돕는 것은 오히려 막강한 소스 코드 편집기다. 편집 중에 객체와 함수의 이름들을 다 기억하고 있다가 알려 주는 편집기야 말로 객체 지향 프로그램을 돕는 놈이다. 이게 없다면 객체, 함수, 변수 이름을 따로 정리해 놓고 있다가 직접 검색 확인해서 입력해야 한다.
헌데 막상 소스 코드를 해석해 보면 함수의 호출 관계를 보여주는 조직도 비슷한 것이 필요하다. 이런 것을 해 주는 프로그램이나 편집기는 아직 없더라. A함수를 해석하는데 의미를 알 수 없는 B함수를 호출하더라. B를 찾아가 보니 또 C를 호출하는데 C를 찾아가면 D를 호출하더라. 뭐 이런 식으로 가니 소스 코드 해석이 어렵더라. 객체도 마찬가지. 이 객체가 다른 객체를 포함하고 있어 찾아가면 또 다른 객체를 포함하고 있고 뭐 그런 식이다.