㈜ 초보의 글이므로 잘못되거나 서투른 표현이 있을 것입니다. 그러나 읽는 분을 위해 최선을....
◆ 결론 : 함수란 "자료를 주고 받는 관계"이다.
【 순서 】 |
순서1 : 함수개념 잡기 |
1. 함수개념 잡기
C언어는 곧 함수라고 할 만큼, 그 기반을 함수에 두고 있다. 함수란 무슨 뜻이며 또 왜 사용하게 되었을까? 한개의 프로그램을 제작할 때는 소스 크기가 크므로 내용들을 여러개로 쪼개서 표현하는 방법 - 흔히 '모듈'이라 부른다 -을 사용하고 있는데 이것이 함수다. 함수의 가치란 한 번 만들어 놓은 함수를 여러번 재활용할 수 있다는 점이다.
C는 프로그래머들을 위해 함수라는 것을 많이 만들어 두었다. C에서 준비해 둔 함수들은 수 백가지가 되는데 이들을 흔히 표준 함수 또는 컴파일러의 내장 함수나 라이브러리 함수라고도 부르고 있다. 그래서 C를 함수들의 집합이라는 말을 쓰기도 함. 그 중 대표적인 함수가 main 함수이다. 또한 위의 함수들 외에 필요하다면 자신이 직접 함수를 만들어 - 이것을 '사용자 정의 함수'라고 부름 - 사용하기도 한다.
함수의 특징이라면 '주고 받는 관계'가 있다는 점이다. 즉, 어떤 자료 내용을 주기도 하고 또 받기도 하는 그런 관계의 성질이 있다는 말이다. 바로 이점이 중요한데 꼭 이해하여야 할 내용이다. 만약 어떤 함수명이 사용된다면 - main 함수 외에는 - 반드시 그 함수 이름이 2번 이상 나오게 되는데 이때 그 하나는 함수를 '호출하는 함수'이며, 또 하나는 그에 응답하여 '호출당하는 함수'이다.
2. 함수의 기본 형태
int aaa(int a , int b)
함수는 위의 내용처럼 함수이름 뒤에 ( )가 사용되는데 이것이 함수명 aaa인 함수이다. 함수의 길이를 따진다면 위의 예보다 더 간단할 수도 있고, 또 더 길 수도 있다.
3. 자료를 주고 받는 관계 이해하기.
단순한 예문을 가지고 연습하자. 우선 전체 소스를 적어본다. 알아 두어야 할 것은 위의 빨간색 내용들이다. 함수명은 편의상 '곱하기'로 표현했다.
void main () |
자, 어떻게 주고 받는다는 것일까? ①의 곱하기 함수가 등장하면서 ②를 부르면 (=호출) ②의 함수는 '예'하고 대답하게 될 것이다. 그러면서 ①은 ②에게 부탁을 한다. 즉, 어떤 자료를 줄터이니, 그것을 잘 활용한 다음에 그 사용 결과를 다시 전달해 주어 자기를 도와달라는 부탁을 한다.
그래서 ①은 a, b라는 자료를 통해 ②의 x, y 로 각각의 내용을 우선 전달해 준다. 그 자료의 크기는 무엇인가? 바로 5와 6이다. 이렇게 하여 5와 6이라는 자료는 각각 x와 y로 전달된다. 그리고, 함수 그 내부에서 이들을 곱하면 z 즉, 30이 되는데 return z이므로 30의 크기를 반환(return) 시키겠다는 뜻이다.
어디로 반환시킬까? 바로 자기에게 부탁을 했던 함수에게 그 값을 다시 돌려드린다! 정확하게 말하면 ①의 c = 곱하기(a, b); 의 a와 b로 전달하여 결국 c로 전해진다. 그래서 printf 함수를 통해 모니터에 c가 30으로 출력된다. 위 흐름이 함수의 주고 받는 관계이다.
위 내용을 문법적인 용어로 재정리 해보자.
1) 함수는 ( )안에 흔히 무언가를 적게 된다. 이들의 이름은 - 보통 인자, 인수, 매개변수 등으로 부르는데 - '인수'라는 용어를 많이 쓰는 것 같다. 또한, ①은 - "호출하는" 함수로써 - ( )안에 들어가는 내용은 '실인수'로, ②는 - "호출당하는" 함수로써 - ( )안에 들어가는 내용은 '가인수'로 불린다.
실인수란 그 변수가 어떤 값을 실제로 가지고 있다는 말이며, 가인수는 - 가짜라는 뜻으로 - 다만 중개 역할만 한다는 뜻으로 이해하면 될 것이다.
2) 그런데 ②의 가인수를 보면, int x, int y로 표현되어 있는데 여기서 2가지를 이해하자. 우선 실인수와 가인수의 - 서로의 이름은 달라도 - 갯수는 일치되어야 한다는 점이다. 그래야 a는 x로, b는 y로 각각 값을 전달시킬 수 있다. 또한, 데이터형 - int, float, char와 같은 것 - 은 서로 일치되어야 한다. 앞에서 a, b를 int로 선언했었기에 ②에서도 역시 int로 표현해 주어야 한다는 점이다.
3) 이번에는 ②의 int 곱하기 (int x, int y) 에서 사용된 int들에 대해서 알아보자. (int x, int y 에서의 int란 자료를 '받을 때'의 데이터형을 나타내는 반면) int 곱하기의 int는 - return를 통해 - 자료를 '다시 반환해 줄 때'의 그 자료형을 나타내는 뜻이다. 따라서 앞쪽의 int만 척 보면 return되는 z가 정수형(= int)의 데이터라는 것을 바로 알 수 있다. 그리고, 함수 ②는 return z를 통해 값을 전달하면서 그 임무를 마치고 종료하게 된다.
그러나, 때로는 return이란 말 자체가 아예 없는 함수도 있다. main 함수가 그 대표적인 예다. 즉, main 함수의 뒷 부분에서는 return 단어를 아예 찾아 볼 수가 없다. main 함수는 - 따라서 노예가 아니라 주인이라고 이해하자! - 다른 함수에게 뭔가를 보고할 의무가 없는 함수이다. 그래서 main 함수 앞쪽에다가 - 처음부터 "나는 돌려줄 게 없는 '주인'이요" 라는 점을 떳떳하게 선언하는 의미로 - void를 쓴 것이다. (물론 main 함수 외에 다른 함수들도 void를 쓰는 경우는 있지만...)
main 함수가 하는 일은 DOS로부터 필요한 인수들을 받아 자신의 일을 행한 후, 다시 DOS로 어떤 값을 되돌려주는 함수이다. 위의 예를 통해 void main(void)란 표현은 (void) 처럼 아무 인수도 DOS로 부터 전달받지 않으며, 또한 되돌려 줄 값도 - void 처럼 - 없는 상황임을 알 수 있다.
4) 또한, return 뒤의 기재되는 내용에는 - 위 예문처럼 z와 같은 값을 쓰는 경우도 있지만 - 그저 단순하게 return; 만으로 표현할 때도 있는데 이때의 그 뜻은 전달해 줄 구체적인 값이 없는 경우로써 다만, 제어권만을 반환하겠다는 뜻이다. 만약 return값이 필요한 경우인데도 불구하고 return해 줄 내용을 내가 적어 주지 않는다면 당연히 에러가 난다.
return 뒤에 기재되는 내용은 상황에 따라 여러가지이다. 즉, 0이나 1 뿐만 아니라 각 상황에 따라 수식이 사용되기도 한다. 수식의 경우에는 - 꼭 필요한 일은 아니지만- ( )처럼 괄호로 묶어주는 것이 관례이다.
4. return이 없는 함수의 예
예문을 보자.
void main() |
=================
가운데 글입니다.
=================
이 소스에서는 design 함수가 ①에서 2번씩이나 호출을 하고 있다. (이처럼 함수를 한 번 만들고 나면 그것을 여러번 사용할 수 있다.) 그리고, 전달해 주는 자료인 인수 즉, 실인수는 없다. 따라서 ②의 호출당하는 함수에서도 어떤 자료를 전달 받아들이게 되는 가인수가 전혀 보이지 않는다. 또한, ② 함수에는 반환(return)시켜 주려는 내용도 아예 없기에 void design() 처럼 void로 표현하게 된 점을 잘 이해할 수 있을 것이다.
//계속
5. 함수 프로터 타입 선언.
위의 3, 4 소스를 실제로 사용하려면 사실은 각각의 소스 앞쪽에다 다음과 같은 것을 미리 적어 주어야 에러가 나지 않는다. 즉,
▶ 위의 3 소스의 경우
int 곱하기 (int x, int y); // 끝에 ; 를 사용하고 있음. (함수의 선언) |
▶ 위의 4 소스의 경우
void design(); // 끝에 ; 를 사용하고 있음. (함수의 선언) |
㈜ 저는 - 일반적인 방법과는 달리 - main 함수를 소스에서 항상 먼저 써 주는 방법을 사용합니다. 그 이유는 비록 위와 같이 main 함수의 앞에다가 사용될 함수들을 선언해 주어야 하는 불편도 생기지만 사실은 이 방법이 초보자에게는 전체 함수의 흐름을 이해하는데는 오히려 매우 큰 도움이 된다고 믿기 때문입니다.
내용은 간단하다. main 함수를 소스의 맨 앞쪽에 위치시켜 적는 방법을 사용하고자 할 때는 사용될 함수를 이처럼 앞쪽에서 미리 신고 (= 함수 선언) 해야 한다는 뜻이다. 이것을 "함수 프로터 타입 선언"이라고 하며, 이 경우에는 함수 끝에다 ;를 써 주어야 한다. 그러나, main 함수를 맨 뒤쪽에 쓸 경우에는 이러한 번거로운 절차가 필요없이 함수를 그냥 사용 (= 함수의 정의)하면 된다.
그밖에 함수안에서 다시 다른 함수를 불러오는 경우도 있다.
6. 전달하는 내용의 종류
함수는 실인수와 가인수를 통해 자료를 전달하고 받음을 공부했다. 그런데, 이 전달 내용에는 다시 다음과 같은 2가지 새로운 개념이 필요하다. 그것이 바로 "값"에 의한 호출(Call by value)과 "참조"에 의한 호출 (Call by reference)이다.
간단하게 생각할 내용이다. 앞에서 살펴본 소스들은 모두 값에 의한 전달 방법이었다. 즉, 구체적인 수치인 값(value)을 사용했었다. 그렇다면 '참조'에 의한 호출이란 무엇인가? 이것은 '주소'를 사용하겠다는 뜻인데, 바로 '포인터'를 사용한다는 말이다. 잘 아시다시피 집에는 주소가 있고 그 안에 사람이 사는 것처럼 메모리(=램)에도 "주소"들이 설정되어 있고, 또 그 안에 "값"을 담고 있다.
정리해 보자면 "값"에 의한 호출이란 메모리의 어느 주소에 저장된 '값'을 중심으로 자료 전달을 하려는 것이고, "참조"에 의한 호출이란 값이 저장된 그 '주소'를 중심으로 따져보려는 차이가 있을 뿐인데 '주소'는 왜 구태여 사용하려는 것일까?
(1) 값에 의한 호출(Call by value)
구체적인 예는 앞에서 살펴보았기에 불필요 할 것이다. 이제 원리적인 측면을 따져보자. 실인수에서 가인수로 자료를 전달할 때 '값'을 사용한다면 기억 장소는 - 주소를 사용하면 1군데인 반면 - 2군데를 사용한다. 즉, 실인수 안의 자료들은 메모리의 어딘가에 이미 위치해 있을 것이다. 그리고, 그 자료들을 넘겨 받게되는 가인수는 또 다른 메모리의 위치에서 그 자료들을 건네받게 되어 - 이때 스택(stack) 방식으로 - 복사된다. 이렇게하여 값을 다룰 경우에는 메모리 기억장소가 2군데 사용된다.
그리고, return이 이루어지고 나면 실인수와 가인수의 관계도 동시에 모두 종결되기 때문에 값에 의한 함수의 호출은 독립성과 안정성이 있는 장점이 있기에 기본적으로 많이 사용되고 있다.
(2) 참조에 의한 호출 (Call by reference)
(값에 의한 호출 방식처럼 - 전달받은 자료를 계산한 다음 return으로 처리해 주는 - 순차적인 방법을 사용하는 단순함과 달리,) 프로그래밍을 하다보면 다음과 같은 처리 방식이 필요할 때도 있다.
즉, 호출당한 함수가 - 전달받았던 자료를 단순하게 계산하여 return 시키지 않고 - 호출한 함수의 실인수를 오히려 거꾸로 바꾸어 버리는 일! 바로 이것이 '참조' 방식이 필요한 이유요, 이 방식에 가장 적합한 방법이 바로 포인터를 사용하는 것인데 결국은 '주소'를 사용하게 되는 것이다. 이와 같이 동일한 '주소'를 사용하기 때문에 참조에 의한 호출은 메모리의 기억장소를 1가지만 사용하게 된다.
▶ 주소 즉, 포인터를 이용한 함수의 호출
void main() |
출력 결과는 10이 아니라 30이 된다. (위 예문이 혹시 잘못 되었는지는 다소 의심되지만) 즉,
① 호출하는 함수의 실인자는 - 변수 a로 단순하게 표현하지 않고 - 주소 연산자인 &를 변수 앞에 썼다. &a의 뜻은 변수 a가 위치한 그 주소를 찾아내는 연산자이므로 - 즉, &은 값이 아니라 주소를 따지겠다는 뜻이므로 - 따라서 a의 값인 10을 전달하지 않고 a의 주소만을 다음의 가인수로 전달해 준다.
② 이때 이 주소를 받게되는 본 함수의 가인수에서는 포인터 표시인 간접연산자인 *를 사용해야 하며, 또 그래서 넘겨온 그 주소를 읽게되는데 - 포인터의 핵심은 주소를 나태내기에 - 이 호출당한 함수내에서 주소인 n에다가 임의로 10이 아닌 다른 값인 30을 넣어준다면 이 30은 *n을 거치고 - 또 &a를 통해서 - 호출을 했던 ①의 실인수인 a의 값인 10을 - 거꾸로 30으로 - 바꾸어 버리게 된다. (return조차도 없어 이러한 결과가 쉽게 이해는 되지 않지만....결과는 실제로 그렇게 출력된다.)
그래서 출력값은 10이 아니라, 30이 된다. 이처럼 포인터를 이용한 주소에 의한 호출을 사용하면 - 1개의 기억 장소만을 사용하면서 - 값도 거꾸로 변경시키는 특징이 있다.
◇ 참고 : right - value와 left- value |
7. 함수 호출과 메모리 영역
stack이란 개념을 이해할 필요가 있다. stack 영역이란 메모리의 한 영역으로써 휘발성이 강하다. 즉, 자료를 일시적으로 기억해 두었다가 함수가 종료되면 이것도 바로 해제되는 성격을 가지고 있으며 또한, 메모리에로 제일 늦게 들어온 내용이 오히려 제일 먼저 사라지는 - FIFO가 아닌 - LIFO의 성질을 가지고 있다.
그렇다면 무엇이 이 stack 영역에 일시적으로 저장될까? 호출을 당하는 함수의 내용들이 들어가게 되는데 그 구체적인 내용이란 가인수는 물론 그 함수 안에서 사용되는 지역변수까지 포함된다.
◇ 참고 :프로그램 정적 영역과 Heap 영역. |
stack이 적용되는 실제 예를 살펴보자. (추후 보완!)
호출당한 함수( = 이것은 '함수1'로 표현하자) 내에서 또 다른 함수를 호출했을 경우에도 또 호출당하는 함수 부분(= 또 이것은 '함수2'로 표현하자)이 있을 것이다. 이때 이들 함수1과 함수2의 해당 내용들이 점차 상위 주소쪽(↓ 방향)으로 저장이 되어나간다는 뜻이다. 즉, 함수1이 우선 저장되고, 그 다음 메모리 영역 ↓ 방향으로 함수2가 저장된다. 그랬다가 함수의 역할을 다 하게되면 함수2부터 stack에서 사라지게 된다는 뜻이다. 따라서 1차로 호출당한 함수가 종료되면 stack도 비워지게 된다.
8. 재귀적 호출
어떤 함수가 함수내에서 또 다시 자기 자신을 부르는 함수의 상태를 재귀적 호출이라고 한다. 이 재귀적 호출을 사용하면 상당히 까다로운 일까지도 아주 간단하게 해결할 수 있다고 한다. 게임 소스에서 실제로 사용될지는 다소 의문이므로, 자세한 설명은 불필요할 것 같다. 재귀적 호출의 단점이란 메모리 공간이 모자라게 될 때는 overflow의 요인이 되기도 한다고 한다.
(끝)

Comments List