본문 바로가기
카테고리 없음

C언어 - 함수와 재귀

by kik328288 2026. 5. 3.

C언어 함수 (재귀, 매개변수, 호출)

프로그램의 규모가 커지면 한 곳에 모든 코드를 몰아넣는 방식으로는 더 이상 관리할 수 없게 된다. 같은 작업을 여러 곳에서 반복하게 되고, 코드를 수정할 때마다 누락이 생기며, 한 사람이 전체를 이해하는 일조차 어려워진다. 이러한 문제를 해결하기 위해 등장한 도구가 바로 함수(Function)이며, C언어를 비롯한 모든 절차형 언어의 가장 기본적인 추상화 단위이다(출처: cppreference — Functions). 본 글은 함수의 정의 방법과 매개변수 전달 방식, 그리고 자기 자신을 호출하는 특별한 형태인 재귀 함수의 동작 원리까지 한 번에 정리한다. 제가 학교 C언어 첫 학기에 가장 헷갈렸던 게 Call by Value와 Call by Reference의 차이였는데, swap 함수를 두 방식으로 직접 짜 본 후 한쪽만 두 변수가 실제로 교환되는 모습을 보고 나서야 매개변수 전달이 단순한 형식이 아니라 메모리 동작 그 자체라는 점을 손끝으로 받아들였다.

 

함수의 개념과 정의 방법

함수란 특정 작업을 수행하는 코드를 하나의 이름으로 묶어 놓은 단위이다. 같은 코드를 여러 번 작성할 필요 없이 함수 이름으로 호출할 수 있게 함으로써 코드의 재사용성을 극대화하고, 큰 프로그램을 작은 단위로 분할해 가독성과 유지보수성을 향상시킨다. 또한 한 함수는 한 가지 일만 수행하도록 작성하는 단일 책임 원칙(Single Responsibility Principle, SRP)을 따를 때 가장 좋은 함수가 된다. 여기서 SRP란 한 함수가 변경되어야 할 이유가 단 하나뿐이어야 한다는 객체지향 설계 원칙의 첫 번째 글자이며, 함수의 길이를 짧게 유지하고 이름을 명확히 짓는 가장 큰 동기가 된다. 우리가 자주 사용하는 printf와 scanf도 모두 C 표준 라이브러리에 정의된 함수이며, 매번 출력 로직을 새로 작성하지 않아도 되는 이유가 바로 함수의 재사용성에 있다.

C언어에서 함수는 크게 선언(Declaration)과 정의(Definition) 두 단계로 작성된다. 선언은 함수가 어떤 형태인지를 컴파일러에 미리 알리는 작업으로, 반환형 함수명(매개변수 목록); 형식의 함수 원형(Prototype)으로 작성된다. 일반적으로 헤더 파일이나 소스 파일 상단에 위치시키며, main 함수 위에 둠으로써 본문에서 자유롭게 호출할 수 있게 만든다. 정의는 실제 함수의 본체를 작성하는 작업으로, 반환형 함수명(매개변수 목록) { 실행할 코드 } 형식이다.

#include <stdio.h>

// 1) 함수 원형 (선언)
int add(int a, int b);

int main(void) {
    int result = add(3, 5);     // 함수 호출
    printf("%d\n", result);     // 8
    return 0;
}

// 2) 함수 정의 (본체)
int add(int a, int b) {
    return a + b;
}

함수는 반환형(Return Type), 함수명, 매개변수 목록, 본문이라는 네 가지 요소로 구성된다. 반환형은 함수가 호출자에게 돌려주는 값의 자료형을 명시하며, 값을 돌려주지 않는 경우 void를 사용한다. 매개변수 목록은 함수가 호출될 때 받아들이는 값들의 자료형과 이름을 나열하며, 받지 않는 경우 비워두거나 void를 명시한다. 본문에서 return 문을 만나면 즉시 함수가 종료되고 호출자에게 값이 반환된다. 가장 단순한 형태의 함수는 main 함수이며, int main(void) { return 0; } 형식이 모든 C 프로그램의 진입점이다.

매개변수 전달 방식 — Call by Value와 Call by Reference

함수에 데이터를 전달할 때는 호출자가 가지고 있는 값을 함수의 매개변수에 어떻게 넘길 것인가가 중요한 설계 결정이 된다. C언어는 두 가지 전달 방식을 지원하며, 시험과 실무 모두에서 자주 출제되는 핵심 개념이다.

전달 방식 매개변수에 전달되는 것 함수 내부의 수정이 호출자에 영향? 대표 사용처
값에 의한 호출(Call by Value) 값의 복사본 영향 없음 일반적인 함수 인자
참조에 의한 호출(Call by Reference) 변수의 주소 원본에 직접 반영 swap·구조체·큰 배열

값에 의한 호출은 매개변수에 전달된 값의 복사본을 함수가 받아 사용하는 방식이다. 호출자의 변수와 함수 내부의 매개변수는 메모리상 완전히 별개의 공간에 존재하며, 함수 내부에서 매개변수의 값을 아무리 수정해도 호출자의 원본 변수에는 어떠한 영향도 미치지 않는다. C언어의 모든 함수 호출은 기본적으로 값에 의한 호출 방식을 따른다.

참조에 의한 호출은 매개변수에 변수의 주소값을 전달하는 방식이다. 엄밀히 말하면 C언어는 참조에 의한 호출을 직접 지원하지 않으며, 포인터(Pointer)와 주소 연산자(&)를 활용해 그 효과를 만들어내는 방식이다. 다음 swap 함수는 정보처리기사 실기에서 매회 출제되는 대표 빈출 예제이다.

#include <stdio.h>
void swap_value(int a, int b) {       // 값 호출 — 효과 없음
    int t = a; a = b; b = t;
}
void swap_ref(int *pa, int *pb) {     // 참조(주소) 호출
    int t = *pa; *pa = *pb; *pb = t;
}
int main(void) {
    int x = 10, y = 20;
    swap_value(x, y);
    printf("%d %d\n", x, y);          // 10 20  (변경 안 됨)
    swap_ref(&x, &y);
    printf("%d %d\n", x, y);          // 20 10  (실제 교환됨)
    return 0;
}

C언어에서 배열은 함수에 전달될 때 자동으로 첫 원소의 주소로 변환되어 전달된다. 따라서 함수 매개변수로 int arr[]라고 작성한 것과 int *arr라고 작성한 것은 컴파일러 입장에서 완전히 동일하며, 함수 내부에서 배열을 수정하면 호출자의 원본 배열도 함께 변경된다. 솔직히 이건 예상 밖이었는데, 제가 학교 첫 학기에 배열을 함수 인자로 넘기면서 내부 변경이 외부에 반영되는 모습을 보고 한참 혼란스러워했지만, "배열 이름은 사실상 첫 원소의 주소"라는 한 줄을 외운 후로는 모든 사고가 한 번에 정리되었다. 호출자의 원본을 보호하면서 배열을 함수에 전달하고 싶다면 const 키워드를 매개변수에 붙여 함수 내부에서 수정하지 못하도록 강제할 수 있다.

재귀 함수의 동작 원리와 실기 예제

재귀 함수(Recursive Function)란 자기 자신을 호출하는 함수를 의미한다. 처음 접하면 코드가 무한히 반복될 것 같은 두려움이 들지만, 실제로는 종료 조건(Base Case)을 명확히 정의하면 정해진 횟수만큼 호출되고 결과를 만들어낸다. 재귀는 본질적으로 큰 문제를 같은 형태의 작은 문제로 분할해 해결하는 사고방식이며, 트리 탐색·정렬 알고리즘·수학적 정의 같은 분야에서 매우 자연스러운 표현 방식을 제공한다.

재귀 함수가 동작하는 원리는 호출 스택(Call Stack)에 있다. 함수가 호출될 때마다 그 함수의 매개변수, 지역 변수, 반환 주소가 담긴 스택 프레임(Stack Frame)이 호출 스택의 가장 위에 쌓인다. 여기서 스택 프레임이란 한 번의 함수 호출에 필요한 모든 정보(지역 변수·매개변수·반환 주소)를 묶어 호출 스택에 적재하는 메모리 블록을 의미한다. 재귀 호출이 발생하면 같은 함수의 새로운 스택 프레임이 다시 쌓이며, 종료 조건을 만족해 반환이 시작되면 스택 프레임이 차례대로 해제되면서 결과 값이 거꾸로 전달된다.

#include <stdio.h>
// 정보처리기사 실기 빈출 — 팩토리얼
int factorial(int n) {
    if (n <= 1) return 1;             // 종료 조건
    return n * factorial(n - 1);      // 재귀 호출
}
// 피보나치 — 재귀의 단순함과 비효율을 동시에 보여 줌
int fib(int n) {
    if (n < 2) return n;
    return fib(n - 1) + fib(n - 2);
}
int main(void) {
    printf("%d\n", factorial(5));     // 120
    printf("%d\n", fib(10));          // 55
    return 0;
}

재귀 함수를 작성할 때는 두 가지를 반드시 지켜야 한다. 첫째, 종료 조건이 반드시 존재해야 하며 모든 재귀 호출이 결국 그 조건에 도달해야 한다. 종료 조건이 없거나 도달할 수 없는 형태로 작성된 재귀 함수는 호출 스택을 끝없이 쌓다가 결국 스택 오버플로(Stack Overflow) 오류를 일으키며 프로그램이 중단된다(출처: K&R The C Programming Language). 둘째, 매번 호출이 종료 조건에 가까워지는 방향으로 진행되어야 한다. n에 대한 재귀라면 매 호출마다 n이 작아지거나 단순해지는 형태가 되어야 한다는 뜻이다.

재귀가 자연스러운 문제와 그렇지 않은 문제는 분명히 구분된다. 피보나치 수열, 팩토리얼, 하노이의 탑, 트리 순회, 분할 정복 알고리즘 같은 수학적·재귀적 정의를 가진 문제는 재귀로 표현하는 것이 코드를 가장 단순하고 직관적으로 만든다. 반면 단순 반복으로 충분히 표현되는 작업이나 깊이가 매우 커질 수 있는 작업은 재귀보다 반복문이 더 안전하고 효율적이다. 제 경험상 학교 알고리즘 과제에서 피보나치를 단순 재귀로 짰다가 n=40만 넣어도 몇 초씩 걸리는 모습을 보고 나서야 단순 재귀의 호출 폭증을 손끝으로 받아들였고, 메모이제이션이나 반복문 변환의 가치를 비로소 이해하게 되었다. 따라서 좋은 프로그래머는 재귀로 표현했을 때 가독성이 명확히 향상되는 경우에만 재귀를 선택하고, 성능이 중요한 영역에서는 같은 알고리즘을 반복문으로 변환하는 능력을 함께 갖추는 것이 권장된다.


메타 디스크립션: C언어의 핵심 추상화 단위인 함수의 정의 방법, 매개변수 전달 방식인 값 호출(Call by Value)과 주소 호출(Call by Reference)의 차이, 그리고 재귀 함수의 동작 원리와 활용을 정보처리기사 실기 빈출 코드 예시와 함께 정리합니다.


소개 및 문의 · 개인정보처리방침 · 면책조항

© 2026 블로그 이름