[C언어] 함수(Function) 개념과 구현방식, 예제
함수(Function)
함수를 중학생때부터 배우죠? 그렇기 때문에 저는 중학교 시절 수학을 포기했습니다. 여러분들은 저보다 뛰어나시니 그렇지는 않았겠죠?
C언어에서 함수는 아주 필수적이라고 할 수 있습니다. 함수에 대해서 간단히 말씀을 드리면 반복되는 코드를 하나로 묶어 필요할때 가져다가 쓴다는 것입니다.
다음의 코드는 어떻게 생각하시나요?
단순히 세 입력의 펙토리얼(!)을 구하여 곱하는 프로그램이지요.
#include <stdio.h> int main() { int fact_a = 1, fact_b = 1, fact_c = 1; int a, b, c; int i; scanf("%d %d %d", &a, &b, &c); for (i = 1; i <= a; i++) fact_a*=i; for (i = 1; i <= b; i++) fact_b *= i; for (i = 1; i <= c; i++) fact_c *= i; printf("%d! * %d! * %d!= %d\n", a, b, c, fact_a*fact_b*fact_c); return 0; }
프로그램이 잘 동작하는지 실행해보세요!
잘 돌아가지요? 문제 없습니다.
하지만 저는 조금 불만인데요. 펙토리얼을 세번 불러오는데, 세번 다 for루프를 돌려야하기 때문에 손가락이 아픈게 그 이유인데요.
만약 10개의 입력으로 들어오고 10개의 펙토리얼의 곱을 구하게 되면 위의 코드에서 for루프를 7개 더 추가해야한다는 것이 매우 불만이지요.
우리가 팩토리얼의 기능을 하는 하나의 코드를 두고 그 코드를 원할때 마다 불러온다면 어떨까요? 그런 역할을 하는 것이 바로 함수입니다.
함수 구현 방식
함수는 어떻게 생겨먹었을까요? 꽤나 간단하게 이해할 수 있을겁니다.
함수의 형태는 이렇습니다.
반환형 함수이름(매개변수1, 매개변수2, ... ){
//몸체
//return 반환값
}
o 반환형 : 반환되는 값의 자료형을 의미합니다. 함수에서는 반환되는 값이 없을 수 있는데요. 그 경우 void를 사용합니다. 만약 반환값이 있다면 return에서 값을 반환시켜 주면 됩니다. 그러니 반환값의 자료형과 반환형이 일치해야됩니다.
o 함수이름 : 함수를 불러올때 사용되는 이름입니다. 여러분이 지어주기 나름인데, 이 이름을 보고 사용하는 사람이 "아~ 이런 기능을 하는 함수겠구나!" 라고 알 수 있도록 잘 지어주어야 합니다.
o 매개변수 : 함수에 대한 input이라고 생각하면 됩니다. 이 매개변수를 토대로 함수의 반환값이 달라질 수 있습니다.
기본적인 매개변수의 동작은 전달받은 인자의 값을 복사하는 것입니다. (단, 포인터와 같은 매개변수는 값의 복사가 아닌 참조를 하게 됩니다.)
o 몸체 : 함수가 어떻게 기능을 할지 로직을 구현하는 부분입니다.
o return : 반환값을 반환하는 명령입니다. return은 제어문으로 여러개 올 수 있습니다. 단, return은 한번만 진행하므로 만약 if 조건에서 return 문을 썼는데, if 조건에 걸리게 된다면 이 후의 코드를 실행시키지 않고 반환합니다.
혹은 반환형이 void이지만 그 함수를 어떤 조건에서 끝내고 싶다면 반환값없이 그냥 return을 사용해주면 그 즉시 함수를 끝냅니다.
우리는 위의 허접한 코드를 factorial이라는 함수를 만들어 조금 더 간편하게 바꿔볼 생각입니다.
위의 형식 그대로 사용해서 factorial을 구현한다면 이렇게 생겼겠죠?
int factorial(int n) { int ret = 1; int i; for (i = 1; i <= n; i++) ret *= i; return ret; }
반환형태는 int형이면서 매개변수는 정수형 n입니다. 비교해보세요. 반환형과 반환값(ret)의 자료형이 일치하는 것을 알 수 있죠?
그 후 메인에서는 이 함수를 호출해서 쓰기만 하면 된답니다.
#include <stdio.h> int factorial(int n) { int ret = 1; int i; for (i = 1; i <= n; i++) ret *= i; return ret; } int main() { int fact_a = 1, fact_b = 1, fact_c = 1; int a, b, c; int i; scanf("%d %d %d", &a, &b, &c); fact_a = factorial(a); fact_b = factorial(b); fact_c = factorial(c); printf("%d! * %d! * %d!= %d\n", a, b, c, fact_a*fact_b*fact_c); return 0; }
어때요? 메인이 훨씬 간결해졌음을 알 수 있습니다.
호출 과정은 다음과 같습니다.
메인 함수를 실행하다가 factorial함수를 만났습니다. 그러면 factorial 함수를 실행시키고 함수가 끝나면 다시 메인함수로 돌아와서 그 전에 실행했던 것을 계속 진행하게 됩니다.
fact_a는 factorial 함수의 반환값이 저장됩니다. 나머지 fact_b, fact_c도 역시 마찬가지구요.
만약 10개의 입력이 주어진다하더라도 factorial만 10번 호출하면 되지요.
(아 물론 이 경우에는 배열과 반복문을 써야하겠지만)
함수 선언
근데 꼭 위에서만 함수를 정의하고 몸체를 구현해야할까요? 그럴필요는 없습니다.
함수를 메인 아래에서 정의할 수도 있습니다.
하지만 꼭 위에서 함수 선언을 해주어야만 합니다. 왜 그러냐구요?
C언어는 절차지향언어이기 때문에 위에서 아래로 실행하기 때문이지요. 그래서 함수가 밑에 정의되어있는데 메인함수에서 그 함수를 호출한다고 하면 컴파일러는 그 함수를 본적이 없으니까 컴파일 에러를 토하게 됩니다.
위의 코드를 함수의 선언 방식으로 코딩해보도록 하면 다음과 같이 간단하게 바뀝니다.
#include <stdio.h> int factorial(int); //함수선언 int main() { int fact_a = 1, fact_b = 1, fact_c = 1; int a, b, c; int i; scanf("%d %d %d", &a, &b, &c); fact_a = factorial(a); fact_b = factorial(b); fact_c = factorial(c); printf("%d! * %d! * %d!= %d\n", a, b, c, fact_a*fact_b*fact_c); return 0; } int factorial(int n) { int ret = 1; int i; for (i = 1; i <= n; i++) ret *= i; return ret; }
함수에 밑에 있군요. 메인함수 위의 선언이 있죠?
선언에서는 매개변수의 자료형만 적어주어도 상관없습니다.
재귀함수
함수에서 자신의 함수를 불러오는 것을 바로 재귀함수라고 합니다. factorial함수는 재귀함수로도 구현할 수 있습니다.
int factorial(int n) { if (n <= 1) return 1; return n*factorial(n - 1); }
factorial함수에서 factorial함수를 호출하는 것을 볼 수 있지요? 매개변수 n과 다음 factorial(n-1)의 반환값을 곱하는 과정을 반복하고 있습니다. factorial의 매개변수 n은 하나씩 줄어들어 결국에는 1 이하가 될겁니다. 그때 1을 반환하지요.
결국 n * (n-1) * (n-2) * ... * 1이 되어 n!을 구현하는 함수죠.
그림으로 보면 더 이해가 쉽게 될겁니다.
3!을 구하는 과정을 보여줍니다. factorial(3)은 factorial(2)를 호출하고 factorial(2)는 factorial(1)을 호출하는 과정을 보여주고 있습니다.
이때 factorial(1)은 if조건문에 걸려 1을 반환하여 더이상 자신을 호출하지 않습니다.
*** 이를 기저 사례라고 합니다.
재귀함수는 시스템의 스택을 사용하고 계속 사용할 경우 stack overflow가 발생할 수 있으므로 되도록이면 반복문을 사용하는 것이 좋습니다.
이상으로 함수에 대해서 기본적인 설명을 해봤습니다.