프로그램을 작은 크기의 함수들로 나눠서 구현하게 되면, 문제의 발생 및 프로그램 소스코드의 변경이 필요한 경우, 변경의 범위를 축소 및 제한할 수 있다.
함수의 기본적인 형태는 아래 그림과 같다.
전달인자의 유무와 반환값의 유무에 따라서 함수를 네 개의 형태로 나눌 수 있다.
int Add(int num1, int num2) // 매개변수 선언
{
int result = num1 + num2;
return result; // result에 저장된 값을 Add 함수를 호출한 영역으로 전달
}
위 함수를 해석해보자면: 두 개의 숫자를 전달 받고, 더해서 나온 값을 result에 저장하여 int형으로 반환하는 Add 함수이다.
매개변수의 선언이란 함수호출 시 전달되는 인자를 저장할 변수의 선언이다. 참고로 함수호출 시 전달할 수 있는 인자의 수는 여러 개가 될 수 있지만, 반환할 수 있는 값의 수는 하나이다.
a = Add(3, 4);
이처럼 함수 호출을 실행했을 때, a에는 result의 값인 7이 저장된다.
이 과정을 함수의 호출문이 반환 값으로 대체되는 것이라고 이해할 수 있다.
함수가 호출되면 호출된 함수의 영역으로 실행의 흐름이 이동하고, 호출된 함수가 반환을 하면 멈췄던 영역으로 다시 돌아와 main함수의 실행을 진행한다.
void ShowAddResult(int num) // 인자전달(ㅇ), 반환값(x)
{
printf("덧셈결과 출력: %d \n", num);
}
ShowAddResult 함수는 반환형이 void로 선언되었다. 여기서 사용된 void에는 '반환하지 않는다'라는 뜻이 담겨있다. 실제로 함수의 몸체 부분에는 return문이 없다.
int ReadNum(void) // 인자전달(x), 반환값(ㅇ)
{
int num;
scanf("%d", &num);
return num;
}
ReadNum 함수에서 사용된 void는 '인자를 전달하지 않는다'라는 뜻이다. 따라서 함수를 호출할 때 인자를 전달하면 안된다.
void HowToUseThisProg(void) // 인자전달(x), 반환값(x)
{
printf("두 개의 정수를 입력하면 덧셈결과가 출력됩니다. \n");
}
HowToUseThisProg 함수엔 전달인자도, 반환값도 존재하지 않는다. 그저 메시지를 출력하는 간단한 함수이기 때문에 전달인자와 반환값이 불필요하다.
int main(void)
{
int result, num1, num2;
HowToUseThisProg();
num1=ReadNum();
num2=ReadNum();
result=Add(num1, num2);
ShowAddResult(result);
return 0;
}
앞서 나왔던 함수들을 모두 사용한 mian 함수이다.
함수의 정의가 함수의 호출문보다 먼저 등장하는 경우에, 컴파일러는 main함수에 존재하는 함수의 호출문을 무리 없이 컴파일한다.
호출문이 먼저 등장한 경우, 위에서 아래로 컴파일을 진행하는 컴파일러는 호출문에 사용된 함수를 본 적이 없으므로 컴파일 에러를 일으킨다.
즉, 함수는 호출되기 전에 미리 정의되어 있어야 한다.
하지만 함수를 정의하지 않고 호출문 전 함수를 미리 선언했다면 에러는 발생하지 않는다.
int Increment(int n); // 함수의 선언
int main(void)
{
int num=2;
num=Increment(num);
return 0;
}
int Increment(int n) // 함수의 정의
{
n++;
return n;
}
지역변수란 중괄호 내에 선언되는 모든 변수이다. 따라서 지역변수는 선언된 지역 내에서만 유효하다.
int SimpleFunc(void)
{
int num=10; // SimpleFunc의 지역변수 num
num++;
printf("SimpleFunc num: %d \n", num);
return 0;
}
함수 SimpleFunc의 지역변수 num은 선언된 이후부터 SimpleFunc 함수를 빠져나가기 직전까지만 유효하다. 지역변수는 해당지역을 벗어나면 자동으로 소멸되기 때문이다.
함수가 여러번 호출된다면 지역변수는 메모리에 할당되고 소멸되기를 반복한다.
또한, 지역변수는 선언된 지역 내에서만 유효하기 때문에 선언된 지역이 다르면 이름이 같아도 문제가 되지 않는다.
지역변수는 반복문이나 조건문에서도 선언이 가능하다.
int main(void)
{
for(int i=0; i<3; i++)
{
int num=0;
num++;
printf("num의 값: %d \n", num);
}
return 0;
}
[실행결과]
num의 값: 1
num의 값: 1
num의 값: 1
위 코드에서 알 수 있듯이 for문에 선언된 지역변수 num은 for문이 반복된다고 해서 값이 증가하지 않는다. for문의 반복은 중괄호 내에서 이루어지는 것이 아닌, 중괄호의 진입과 탈출을 반복하면서 이루어지기 때문이다.
함수를 정의할 때 선언한는 매개변수도 지역변수의 일종이다.
선언된 함수 내에서만 접근이 가능하고, 선언된 함수가 반환을 하면 소멸이 된다.
전역변수는 지역변수와 달리 중괄호 내에 선언되지 않는다. 전역변수는 프로그램의 시작과 동시에 메모리 공간에 할당되어 종료 시까지 존재하며, 별도의 값으로 초기화하지 않으면 0으로 초기화된다. 또한 프로그램 전체 영역 어디서든 접근이 가능하다.
만약 전역변수와 동일한 이름의 지역변수가 선언된다면, 해당 지역 내에서는 전역변수가 가려지고 지역변수로의 접근이 이뤄진다.
int Add(int val)
{
int num=9;
num+=val;
return num;
}
int main(void)
{
int num=5;
printf("num: %d \n", Add(3)); // 지역변수 num을 사용
printf("num: %d \n", num+9); // 전역변수 num을 사용
return 0;
}
[실행결과]
num: 12
num: 14
가급적이면 전역변수와 지역변수의 이름은 달리하는 것이 좋다.
함수 내에 선언된 지역변수에 static 선언이 붙게 되면, 그 변수는 선언된 함수 내에서만 접근 가능하며, 딱 1회 초기화되고 프로그램 종료 시까지 메모리 공간에 존재한다.
static변수는 프로그램이 시작할 때 메모리에 할당되는 전역변수의 특징을 가지며, 선언된 지역 내에서만 접근 가능한 지역변수의 특징을 갖고 있는 것이다.
void SimpleFunc(void)
{
static int num1=0; // static 변수
int num2=0; // 지역변수
num1++, num2++;
printf("static: %d, local: %d \n", num1, num2);
}
int main(void)
{
for(int i=0; i<3; i++)
SimpleFunc();
return 0;
}
[실행결과]
static: 1, local: 1
static: 2, local: 1
static: 3, local: 1
실행결과를 보면 알 수 있듯이, static변수는 프로그램이 종료될 때까지 메모리에서 소멸되지 않아 수가 증가하지만, 지역변수는 함수 탈출과 동시에 소멸되므로 수가 증가 하지 않는다.
register 변수를 선언하면 CPU내에 존재하는 '레지스터'라는 메모리 공간에 저장될 확률이 높아진다. 이 변수는 자주 사용할 것이니 접근이 가장 빠른 레지스터에 저장하라는 신호를 주는 것이다.
하지만 컴파일러가 이 변수의 선언이 합당하지 않다고 판단하면 레지스터에 할당되지 않는다. 레지스터는 비싸고 중요한 메모리 공간이기 때문에 register 변수는 거의 사용하지 않는다.
재귀함수란 함수 내에서 자기 자신을 다시 호출하는 함수를 의미한다.
void Recursive(void)
{
Recursive(); // 자기 자신을 호출
}
Recursive 함수를 실행하는 중간에 다시 Recursive 함수가 호출되면, Recursive 함수의 복사본을 하나 더 만들어서 복사본을 실행하게 된다. 하지만 위의 코드를 실행하면 '재귀의 탈출조건'이 없기 때문에 무한 루프에 빠지게 된다.
void Recursive(int num)
{
if(num<=0) // 재귀의 탈출 조건
return; // 재귀의 탈출
Recursive(num-1);
}
int main(void)
{
Recursive(3);
return 0;
}
위 코드를 실행하면 재귀적으로 호출이 이루어지고 있는 Recursive 함수에 0이 전달되면서 재귀의 탈출조건이 성립되어 함수가 반환하기 시작한다. 호출 - 호출 - 호출 - 반환 - 반환 - 반환의 순서를 갖는 것이다. 이렇듯 재귀함수를 정의하려면 탈출조건을 구성해야 한다.
재귀함수는 자료구조나 알고리즘의 어려운 문제를 단순화할 수 있으며, 재귀적인 수학적 수식을 그대로 코드로 옮길 수 있다.