6. 함수로 코드를 나누고 재사용하기
지금까지는 main() 안에 코드를 차곡차곡 쌓아 가며 프로그램을 만들었다. 처음에는 이 방식이 단순해서 편하다. 하지만 프로그램이 조금만 길어지면, 어디서 무엇을 하는지 찾기 어려워지고, 같은 코드를 여러 번 복사해서 붙여 넣게 된다. 이때 등장하는 아주 든든한 도구가 바로 함수다.
함수는 한마디로 말해 이름이 붙은 작은 작업 상자다. 계산을 하거나, 화면에 출력하거나, 어떤 값을 검사하는 일을 따로 떼어 상자에 넣어 두고 필요할 때마다 꺼내 쓰는 것이다. 함수를 잘 쓰기 시작하면 코드는 짧아지고, 읽기 쉬워지고, 고치기도 쉬워진다.

함수는 왜 필요한가
예를 들어 학생 30명의 점수 평균을 계산하는 프로그램을 만든다고 해 보자. 평균 계산식이 여러 곳에서 반복된다면, 나중에 계산 방식을 바꿔야 할 때 모든 위치를 다 수정해야 한다. 하지만 평균 계산을 함수 하나로 만들어 두면, 그 함수만 고치면 된다.
함수의 장점은 분명하다.
반복되는 코드를 줄일 수 있다.
긴 프로그램을 작은 조각으로 나눌 수 있다.
각 조각의 역할이 분명해져 읽기 쉬워진다.
수정할 때 한 곳만 바꾸면 되는 경우가 많아진다.
같은 기능을 여러 번 재사용할 수 있다.
초보자일수록 함수를 빨리 익히는 것이 좋다. 함수는 어려운 장식이 아니라, 복잡함을 줄이는 가장 기본적인 정리 습관이기 때문이다.
함수의 기본 모양: 선언, 정의, 호출
C에서 함수를 이해할 때는 세 단어를 기억하면 된다. 선언, 정의, 호출이다.
선언은 "이런 함수가 나중에 있을 거예요" 하고 컴파일러에게 미리 알려 주는 것이다. 정의는 함수가 실제로 무슨 일을 하는지 적는 것이다. 호출은 그 함수를 실제로 사용하는 것이다. C는 소스 코드를 위에서 아래로 읽기 때문에, main()보다 아래쪽에 함수를 정의할 거라면 보통 먼저 선언을 적어 둔다.1
다음 예제를 보자.
#include <stdio.h>
int add(int a, int b); // 함수 선언
int main(void)
{
int result = add(3, 5); // 함수 호출
printf("결과: %dn", result);
return 0;
}
int add(int a, int b) // 함수 정의
{
return a + b;
}이 코드를 차례대로 읽어 보자.
먼저 int add(int a, int b);는 add라는 함수가 있고, 정수 두 개를 받아서 정수 하나를 돌려준다고 알려 준다. 그다음 main()에서 add(3, 5)를 호출한다. 그러면 프로그램은 add 함수로 가서 a + b를 계산한 뒤 return으로 결과를 돌려준다. 그리고 다시 main()으로 돌아와 그 값을 result에 저장한다.
여기서 return은 단순히 값을 돌려주는 것만 하는 게 아니다. 함수 실행을 끝내고 호출한 곳으로 돌아가게 만든다. 그래서 return 아래에 적은 코드는 실행되지 않는다.
인자가 없는 함수는 다음처럼 void를 써서 명확하게 표현하는 습관이 좋다.1
void print_line(void);이 표현은 "아무 값도 받지 않는다"는 뜻이다.
매개변수와 인자, 반환값
함수는 입력을 받고, 일을 하고, 필요하면 결과를 돌려준다. 이 흐름을 이해하면 함수가 훨씬 자연스럽게 보인다.
함수 정의 쪽에 적는 변수는 매개변수이고, 함수를 호출할 때 실제로 넘기는 값은 인자다. 이름이 비슷해서 헷갈리지만, 매개변수는 함수 입장에서의 입력 칸이고, 인자는 실제로 넣는 값이라고 생각하면 쉽다.
예를 들어 아래 코드에서 x, y는 매개변수이고, 10, 20은 인자다.
int add(int x, int y)
{
return x + y;
}int result = add(10, 20);함수는 인자와 반환값의 유무에 따라 여러 형태로 만들 수 있다.2
인자를 받고 값을 반환하는 함수
인자를 받고 값을 반환하지 않는 함수
인자를 받지 않고 값을 반환하는 함수
인자도 없고 반환값도 없는 함수
가장 많이 쓰는 형태는 "인자를 받고 값을 반환하는 함수"다. 계산 결과를 다른 곳에서 활용하기 좋기 때문이다.
float to_celsius(float fahrenheit)
{
return (5.0f / 9.0f) * (fahrenheit - 32.0f);
}이 함수는 화씨 온도를 받아 섭씨 온도로 바꿔 준다.
float c = to_celsius(98.6f);
printf("섭씨 온도: %.2fn", c);반대로 어떤 함수는 결과를 계산해서 돌려주기보다, 그냥 행동만 할 수도 있다. 예를 들어 화면에 메시지를 출력하는 함수는 void를 반환형으로 쓰는 경우가 많다.
void greet(char name[])
{
printf("안녕하세요, %s님!n", name);
}이 함수는 이름을 받아 인사말을 출력하지만, 호출한 쪽에 따로 돌려주는 값은 없다.
초보자가 자주 하는 실수는 반환형과 return 값의 타입을 맞추지 않는 것이다. int를 반환하겠다고 해 놓고 문자열을 돌려주거나, void 함수인데 값을 return하려고 하면 문제가 된다. 함수의 머리 부분에 적은 반환형은 약속이라고 생각하면 된다.
C의 기본은 값에 의한 전달이다
함수를 배우면서 꼭 알아야 할 중요한 사실이 하나 있다. C는 기본적으로 값을 복사해서 함수에 넘긴다. 이것을 값에 의한 전달이라고 한다.2
말이 조금 딱딱하지만 뜻은 단순하다. 함수에 변수를 넘기면, 원래 변수 자체가 들어가는 것이 아니라 그 값의 복사본이 들어간다는 뜻이다.
#include <stdio.h>
void double_value(int n)
{
n = n * 2;
printf("함수 안의 n: %dn", n);
}
int main(void)
{
int value = 10;
printf("호출 전 value: %dn", value);
double_value(value);
printf("호출 후 value: %dn", value);
return 0;
}이 코드를 실행하면 함수 안의 n은 20이 되지만, main()의 value는 여전히 10이다. 함수는 복사본만 바꿨기 때문이다.
이 성질은 처음에는 조금 야속하게 느껴질 수 있다. "분명 함수 안에서 2배로 만들었는데 왜 원래 값은 안 바뀌지?" 하고 당황하기 쉽다. 하지만 오히려 이 덕분에 함수가 바깥 변수를 함부로 망가뜨리지 않는다.
그렇다면 원래 값을 정말 바꾸고 싶을 때는 어떻게 할까? 그럴 때는 값이 아니라 주소를 넘겨야 한다. 주소를 다루려면 포인터가 필요하다. 지금은 "원본을 바꾸고 싶으면 주소를 넘겨야 한다" 정도만 기억하면 충분하다. 자세한 내용은 포인터 장에서 본격적으로 다룬다.
맛보기로만 보면 이런 모양이다.
void double_value(int *n)
{
*n = *n * 2;
}호출은 이렇게 한다.
double_value(&value);지금 단계에서는 기호가 조금 낯설어도 괜찮다. 중요한 핵심은 이것이다. C 함수는 기본적으로 원본이 아니라 복사본을 받는다.
함수 안의 변수는 어디까지 보일까
함수와 함께 꼭 이해해야 하는 개념이 변수의 범위, 즉 스코프다. 스코프는 변수를 어디에서 사용할 수 있는지를 뜻한다.3
함수 안에서 만든 변수는 기본적으로 그 함수 안에서만 보인다. 다른 함수는 그 변수를 볼 수 없다. 마치 각 함수가 자기만의 서랍을 갖고 있는 것과 비슷하다.
대표적인 변수 범위는 다음 세 가지로 이해하면 된다.
지역 변수: 함수나 블록 안에서 선언한 변수로, 그 안에서만 사용된다.
전역 변수: 모든 함수 바깥에서 선언한 변수로, 여러 함수에서 사용할 수 있다.
static지역 변수: 함수 안에 있지만, 함수 호출이 끝나도 값이 유지되는 특별한 지역 변수다.
먼저 지역 변수부터 보자.
#include <stdio.h>
void show_number(void)
{
int num = 7;
printf("show_number의 num: %dn", num);
}
int main(void)
{
show_number();
return 0;
}여기서 num은 show_number() 안에서만 쓸 수 있다. main()에서 num을 직접 쓰려고 하면 오류가 난다.
전역 변수는 모든 함수 바깥에서 만든다.
#include <stdio.h>
int score = 100;
void print_score(void)
{
printf("점수: %dn", score);
}
int main(void)
{
print_score();
printf("main의 점수: %dn", score);
return 0;
}전역 변수는 편리하지만, 너무 많이 쓰면 코드가 얽히기 쉽다. 어느 함수가 값을 바꿨는지 추적하기 어려워지기 때문이다. 초보자에게는 특히 "필요할 때만 아주 조심해서" 쓰는 습관이 좋다.3
이제 static 지역 변수를 보자.
#include <stdio.h>
void say_hello(void)
{
static int count = 0;
count++;
printf("%d번째 인사입니다.n", count);
}
int main(void)
{
say_hello();
say_hello();
say_hello();
return 0;
}보통 지역 변수는 함수가 끝나면 사라진다. 하지만 static을 붙이면 값이 유지된다. 그래서 위 코드는 호출할 때마다 count가 다시 0이 되는 것이 아니라 1, 2, 3으로 증가한다.
또 하나 기억할 점이 있다. 안쪽 블록에서 같은 이름의 변수를 다시 선언하면 바깥 변수가 가려질 수 있다. 이를 변수 가림이라고 생각하면 된다. 같은 이름을 남발하면 사람이 읽을 때도 헷갈리므로, 초보 단계에서는 이름을 명확하게 다르게 짓는 편이 좋다.3
재귀: 함수가 자기 자신을 부를 때
재귀는 함수가 자기 자신을 다시 호출하는 방식이다. 처음 보면 조금 신기하고, 살짝 무섭기도 하다. 함수가 자기 자신을 또 부른다고? 끝이 없을 것 같은 느낌이 들기 때문이다. 그래서 재귀에서는 반드시 멈추는 조건이 있어야 한다. 이것을 기저 조건 또는 종료 조건이라고 한다.4
가장 유명한 예는 팩토리얼이다. 5!는 5 × 4 × 3 × 2 × 1이다. 이를 재귀로 표현하면 이렇게 된다.
int factorial(int n)
{
if (n <= 1)
return 1;
return n * factorial(n - 1);
}흐름을 말로 풀어 보면 이렇다.
factorial(4)는 4 * factorial(3)을 계산한다. 그런데 factorial(3)은 다시 3 * factorial(2)를 계산한다. 이런 식으로 계속 내려가다가 factorial(1)이 되면 더 이상 자기 자신을 부르지 않고 1을 반환한다. 그러면 그 결과가 차곡차곡 되돌아오면서 최종 결과가 만들어진다.
재귀는 수학 정의를 코드로 옮길 때 아주 자연스럽다. 하지만 모든 반복 문제를 재귀로 푸는 것이 좋은 것은 아니다. 간단한 반복은 for나 while이 더 직관적이고 효율적인 경우가 많다. 특히 재귀 호출이 너무 깊어지면 메모리를 많이 사용해 문제가 생길 수 있다. 종료 조건이 빠지면 함수가 끝없이 자기 자신을 부르다가 프로그램이 멈출 수도 있다.4
그래서 초보 단계에서는 재귀를 이렇게 이해하면 충분하다.
재귀는 "문제를 더 작은 같은 문제로 나누어 해결하는 방식"이다. 그리고 반드시 멈추는 조건이 있어야 한다.
좋은 함수는 어떻게 만들까
함수를 많이 만들기만 한다고 좋은 코드가 되는 것은 아니다. 작고 분명한 함수를 만드는 습관이 중요하다.
함수 하나는 가능하면 한 가지 일만 하게 하자.
함수 이름은
do_work보다calculate_total처럼 역할이 드러나게 짓자.입력이 필요하면 매개변수로 받고, 결과가 필요하면 반환값으로 돌려주자.
함수 안에서 너무 많은 전역 변수를 건드리지 말자.
비슷한 코드가 두 번 보이면 함수로 빼도 되는지 생각해 보자.
예를 들어 합계를 계산하는 함수가 있는데 그 안에서 입력도 받고 출력도 하고 파일 저장까지 한다면, 나중에 다시 쓰기 어렵다. 반면 계산만 하는 함수는 다른 상황에서도 쉽게 재사용할 수 있다.
초보자에게 특히 좋은 습관은 "출력하는 함수"와 "계산하는 함수"를 구분하는 것이다. 계산 함수는 값을 반환하고, 출력은 main()이나 별도의 출력 함수가 담당하면 프로그램 구조가 훨씬 또렷해진다.
직접 해 보면 더 빨리 익는다
이 장을 읽고 바로 손으로 쳐 보면 함수 감각이 훨씬 빨리 생긴다. 다음 정도를 직접 만들어 보면 좋다.
두 수의 큰 값을 반환하는 함수
점수를 받아 등급을 출력하는 함수
원의 넓이를 계산하는 함수
숫자를 받아 1부터 그 숫자까지의 합을 재귀로 구하는 함수
호출 횟수를 세는
static지역 변수를 가진 함수
한 문제를 main()에 전부 쓰는 버전과, 함수로 나눈 버전을 둘 다 만들어 비교해 보면 차이가 확실히 느껴진다. 함수 버전이 처음에는 조금 길어 보여도, 읽을 때는 훨씬 편하다.
마무리
함수는 C 언어에서 코드를 정리하는 가장 강력한 기본 도구다. 선언과 정의, 호출의 흐름을 이해하고, 매개변수와 반환값으로 데이터를 주고받는 법을 익히면 프로그램이 훨씬 구조적으로 바뀐다.
또한 C는 기본적으로 값을 복사해서 함수에 넘긴다는 점, 함수 안의 변수는 범위가 정해져 있다는 점, 재귀는 종료 조건이 꼭 필요하다는 점까지 이해하면 함수의 핵심 뼈대를 잡은 것이다.
이제부터 코드를 짤 때는 이렇게 생각해 보자. "이 부분은 하나의 함수로 뺄 수 없을까?" 이 질문을 자주 던지는 순간, 여러분의 C 코드는 점점 더 짧고, 읽기 쉽고, 믿을 만한 코드가 된다.
참고
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.