함수
C 프로그래밍 관점에서 실행 가능한 모든 코드는 함수 내에 존재한다. 다른 프로그래밍 언어들에선 함수, 서브루틴, 서브프로그램, 프로시저, 메서드 등을 구분할 수 있지만 C언어에서는 모두 함수에 속한다. 함수는 고급 프로그래밍 언어의 기본적인 기능이며, 크고 복잡한 작업들을 더 작고 관리하기 쉬운 코드로 분할하여 작업을 수월하게 처리할 수 있게 한다.
하위 수준에서 함수는 함수와 관련된 명령이 컴퓨터의 메모리에 존재하는 메모리 주소에 지나지 않는다. 소스 코드에서 이 메모리 주소는 함수를 호출하고 함수의 시작 주소에서 시작하는 명령어를 실행하는 데 사용할 수 있는 설명적인 이름을 받는다. 함수와 관련된 명령어들은 종종 코드 블록이라는 명칭으로 불린다. 함수의 명령어 실행이 끝나면, 함수는 값을 반환할 수 있으며 실행된 함수가 호출된 직후의 바로 다음 명령어와 함께 코드 실행이 재개된다. 지금 당장 이해하지 못한다 하더라도 걱정하지 않아도 된다. 컴퓨터 내부에서 일어나는 일을 가장 낮은 수준에서 이해하는 게 처음엔 혼란스러울 수도 있지만 당신이 C 프로그래밍 기술을 개발하다보면 매우 직관적으로 이해할 수 있을 것이다.
지금은 함수와 함수에 관련된 코드 블록들이 종종 여러 곳의 다른 장소에서 여러 번 호출되고 실행된다는 것만 이해하면 충분하다.
기본적인 예시로, 당신이 주어진 (x,y) 점의 x축과 y축에 대한 거리를 계산하는 프로그램을 작성해야 한다고 가정한다. 당신은 정수 x와 y의 절대값을 계산해야 한다. (라이브러리에 절대값에 대한 사전 정의된 함수가 없다고 가정하면) 이렇게 쓸 수 있다.
#include <stdio.h>
/*이 함수는 전체 숫자의 절대값을 계산한다*/
int abs(int x)
{
if (x>=0) return x;
else return -x;
}
/*이 프로그램 위에서 정의한 abs() 함수를 두 번 호출한다.*/
int main()
{
int x, y;
printf("Type the coordinates of a point in 2-plane, say P = (x,y). First x=");
scanf("%d", &x);
printf("Second y=");
scanf("%d", &y);
printf("The distance of the P point to the x-axis is %d. \n Its distance to the y-axis is %d. \n",
abs(y), abs(x));
return 0;
}
다음 예제는 함수를 프로시저로서 사용하는 방법을 보여준다. 학생들에게 3개 수업들의 성적을 묻고, 합격 여부를 알려주는 간단한 프로그램이다. 여기서 우리는 필요한 만큼 호출할 수 있는 check()
라는 함수를 만들었다. 이 함수를 정의 함으로써 학생이 수강한 각 수업들에 대해서 동일한 명령어 세트를 작성하지 않아도 된다.
#include<stdio.h>
/*'체크'함수는 여기서 정의된다.*/
void check(int x)
{
if (x<60)
printf("Sorry! You will need to try this course again.\n");
else
printf("Enjoy your vacation! You have passed.\n");
}
/*프로그램은 '체크' 함수를 세 번 호출하는 메인 함수에서 시작한다.*/
int main()
{
int a, b, c;
printf("Type your grade in Mathematics (whole number). \n");
scanf("%d", &a);
check(a);
printf("Type your grade in Science (whole number). \n");
scanf("%d", &b);
check(b);
printf("Type your grade in Programming (whole number). \n");
scanf("%d", &c);
check(c);
/* 이 프로그램은 좀 더 유용한 것으로 대체해야 한다.*/
return 0;
}
위의 프로그램에는 ‘확인’ 함수에 대한 결과값이 없다는 점을 주목하라. 단지 프로시저만 실행한다.
이것이 함수들이 정확히 무엇을 위한 것인지 알려준다.
함수에 대하여
[+/-]함수를 공장의 기계로 개념화하면 이해하기에 유용하다. 기계의 입구에 처리하고자 하는 “원재료” 또는 입력 데이터를 집어 넣는다. 그런 다음 기계가 작업을 처리하고 완성한 제품, 즉 “반환값”을 기계의 출구로 내보낸 다음 다른 용도로 사용할 수 있다.
C에서는 기계가 어떤 원재료를 가공하게 될 것인지, 그리고 어떤 종류의 완제품을 생산하기를 원하는지 기계에게 정확하게 말해야 한다. 만약 당신이 기계에게 예상한 것과 다른 원재료를 넣거나 당신이 생산하라고 말한 것과 다른 완제품을 반환한다면 C 컴파일러는 오류를 발생할 것이다.
어떠한 입력을 받기 위해 함수가 필요하지 않다는 것에 주의하라. 그리고 또한 당신에게 어떤 출력값도 돌려주지 않는다. 위의 예시를 수정하여 만약 우리가 사용자에게 check
함수 내에서의 성적을 물어본다면, 성적값을 함수에 전달할 필요가 없다. 그리고 check
함수는 합격 여부를 돌려주지도 않는다는 점을 주목하라. 이 함수는 단지 메시지를 화면에 출력할 뿐이다.
함수와 관련된 몇가지 기본 용어를 숙지해야 한다.
- 다른 함수 g를 사용하는 함수 f를 g라고 한다. 예를 들어, f는 g를 호출하여 10개의 숫자의 제곱을 출력한다. 이 때 f는 caller 함수, g는 callee 함수라고 한다.
- 함수에 보내는 입력을 함수의 인자(또는 전달인자)라고 한다. 함수를 선언할 때, 먼저 함수에 전달할 수 있는 유형의 인자를 결정하는 매개 변수를 정의한다. 함수의 이름 옆에 있는 괄호 안의 컴파일러에 이러한 매개 변수를 정의한다.
- 어떤 종류의 답들을 f로 되돌려주는 함수 g는 답이나 값을 반환하는 기능을 한다. 예를 들어, g는 인자의 값을 반환한다.
C에서의 함수 작성
[+/-]예시를 보면서 익히는 것은 언제나 도움이 된다. 숫자의 제곱값을 반환하는 함수를 작성해보자.
int square(int x)
{
int square_of_x;
square_of_x = x * x;
return square_of_x;
}
이와 같은 함수를 작성하는 법을 이해하려면 이 함수가 전체적으로 어떻게 수행되는지 살펴보는 것이 도움이 될 것이다. int x로 매개변수를 정의하고, x의 제곱을 수행한 뒤 그 값을 변수 square_of_x에 저장한다. 그리고 그 값이 출력갑으로 반환된다.
함수의 시작 부분에서 첫 번째 int는 함수가 반환하고자 하는 데이터의 유형이다. 이 예시에서 정수를 제곱하면 정수가 나오고, 정수를 반환하므로 우리는 int를 사용하여 반환 유형을 정수로 정의한다.
다음은 함수의 이름을 정하는 일이다. 함수에 어울리는 의미를 가지고 있으며 서술적인 이름을 사용하는 것이 좋다. 이미 작성된 작업의 이름을 따서 이름을 작성하는 것도 하나의 방법이다. 이 경우 함수 이름을 "square"라고 지정하는데 이는 함수가 수행하는 작업이 숫자를 제곱하는 것이기 때문이다.
중괄호 사이에는 함수의 실제 내용들이 들어있다. x의 제곱값을 고정하는 데에 사용되는 square_of_x라는 정수 변수를 선언한다. square_of_x라는 변수는 오직 이 함수 내에서만 사용할 수 있음에 주의하라. 이 내용에 대해서는 나중에 더 배우겠지만 이 특징이 매우 유용하다는 것을 알 수 있을 것이다.
그런 다음 x에 x를 곱한 값, 또는 x 제곱을 변수 square_of_x로 할당한다. 이 내용이 square
함수의 전부이다.
다음은 반환 과정이다. x의 제곱값을 반환받기 위해서, 우리는 이 함수가 변수 square_of_x의 내용을 반환한다고 선언해야 한다.
이 선언이 끝났음을 중괄호를 닫아서 알린다.
이 코드는 보다 간결한 방식으로 위 코드와 동일한 작업을 수행한다.
int square(int x)
{
return x * x;
}
우리는 이런 함수에 익숙해져야 한다.
일반적인 방법
[+/-]일반적으로 함수를 선언하기 위해서는 다음과 같이 작성한다.
type name(type1 arg1, type2 arg2, ...)
{
/* code */
}
앞서 함수는 인자를 사용하거나 반환, 또는 둘다 할 수 없다고 언급했다. 함수가 아무것도 반환하지 않으려면 함수를 어떻게 작성해야 하는가? 우리는 C의 void 키워드를 사용한다. void는 기본적으로 “무(無)”를 뜻한다. 그래서 우리가 아무것도 반환하지 않는 함수를 쓰고 싶다면 다음과 같이 쓸 수 있다.
void sayhello(int number_of_times)
{
int i;
for(i=1; i <= number_of_times; i++) {
printf("Hello!\n");
}
}
위의 함수에는 반환문이 없다는 점을 주목하자. 그래서 우리는 void를 반환 유형으로 사용한다. (실제로 return 키워드를 사용하여 프로시저가 종료되기 전에 caller에게 돌아갈 수 있지만, 함수처럼 값을 반환할 수는 없다.) 인자를 사용하지 않는 함수는 어떠한가? 이러한 함수는 예를 들어 이렇게 쓸 수 있다.
float calculate_number(void)
{
float to_return=1;
int i;
for(i=0; i < 100; i++) {
to_return += 1;
to_return = 1/to_return;
}
return to_return;
}
이 함수는 입력값을 받지 않고 함수에 의해 계산된 숫자만 반환한다.
당신은 유효한 함수를 얻기 위해 자연스럽게 인자의 void return과 void를 함께 결합할 수 있을 것이다.
재귀 함수
[+/-]여기 무한 루프에 빠진 간단한 함수가 있다. 이 함수는 문장을 출력하고 자신을 호출하는 작업을 반복하고 있다. 이 작업은 스택 오버플로가 발생하고 프로그램이 충돌할 때까지 계속된다. 스스로 호출하는 함수를 재귀 함수라고 하며, 일반적으로 사람이 셀 수 있는 유한한 단계 이후 재귀를 멈추는 조건을 달아둔다.
// 실행하지 마시오!
void infinite_recursion()
{
printf("Infinite loop!\n");
infinite_recursion();
}
간단한 점검은 다음과 같이 할 수 있다. ++depth가 사용되므로 값이 함수로 전달되기 전에 증분이 발생한다는 점에 주의하라. 또는 재귀 호출 전에 별개의 줄에서 증분이 발생할 수도 있다. 만약 당신이 print_me(3,0)이라고 입력한다면, 함수는 재귀 행을 3번 출력한다.
void print_me(int j, int depth)
{
if(depth < j) {
printf("Recursion! depth = %d j = %d\n",depth,j); //j는 값을 저장
print_me(j, ++depth);
}
}
재귀 함수는 디렉토리 트리 스캔, 링크 리스트의 끝 탐색, 데이터베이스의 트리 구조 구문 분석, 숫자 인수분해(그리고 소수 찾기) 등과 같은 작업에 가장 많이 사용된다.
정적 함수
[+/-]함수를 선언된 파일 내에서만 호출하려면 정적 함수로 선언하는 것이 적절하다. 함수가 정적 함수로 선언된다면, 컴파일러는 다른 파일에 있는 코드의 함수가 호출되는 것을 방지하기 위해 오브젝트 파일을 컴파일해야한다는 것을 알게 될 것이다.
static int compare( int a, int b )
{
return (a+4 < b)? a : b;
}
C 함수의 사용
[+/-]우리는 함수를 작성하는 방법을 배웠다. 하지만 어떻게 사용해야 하는가? 우리가 main을 쓸 때, 그 main의 중괄호 밖에 함수를 배치한다. 예를 들어 위의 calculate_number 함수를 사용하여 다음과 같은 함수를 작성할 수 있다.
float f;
f = calculate_number();
만약 함수가 인자를 입력받는다면, 우리는 다음과 같은 함수를 작성할 수 있다.
int square_of_10;
square_of_10 = square(10);
만약 함수가 아무 출력값도 반환하지 않는다면, 우리는 그냥 말하면 된다.
say_hello();
반환값을 내놓는 데에 변수가 따로 필요하지 않기 때문이다.
C 표준 라이브러리에서의 함수
[+/-]C 언어 자체에서는 함수를 포함하지 않지만, 보통 C 표준 라이브러리와 연동된다. 이 라이브러리를 사용하기 위해선 C 파일의 맨 위에 #include 지시문을 추가해야 한다.(이 지시문은 C89/C90 중 하나이다.)
사용할 수 있는 기능은 다음과 같다.
<assert.h> | <limits.h> | <signal.h> | <stdlib.h> |
---|---|---|---|
|
|
|
|
<ctype.h> | <locale.h> | <stdarg.h> | <string.h> |
|
|
|
|
<errno.h> | <math.h> | <stddef.h> | <time.h> |
|
|
|
|
<float.h> | <setjmp.h> | <stdio.h> | |
|
|
|
|
가변 길이의 매개변수 리스트
[+/-]가변 인자 리스트를 가지고 있는 함수는 다양한 수의 인자를 사용할 수 있다. C 표준 라이브러리의 예시로 printf 함수가 있으며, 프로그래머가 함수를 어떻게 사용하는지에 따라 임의의 수의 전달인자를 취할 수 있다.
C 프로그래머는 가변 인자로 새로운 함수를 작성할 필요가 거의 없다. 만약 함수에 한 번에 많은 값을 입력하기를 원한다면, 일반적으로는 링크 리스트나 어레이와 같은 모든 것을 담을 수 있는 구조를 정의하여 인자의 데이터로 함수를 호출한다.
그러나 가끔 가변 인자 리스트를 지원하는 새로운 함수를 작성해야 하는 경우가 생긴다. 가변 인자 리스트를 사용할 수 있는 함수를 만들기 위해서는 먼저 표준 라이브러리 헤더 <stdarg.h>를 포함해야 한다. 그 다음 평소처럼 함수를 선언한다. 그 다음 평소처럼 함수를 선언한다. 그리고 마지막 인자로 ellipsis(“...”)를 추가한다. 이것은 컴파일러에게 변수 전달인자의 리스트가 뒤따라 와야한다는 사실을 알려준다. 예를 들어, 다음 선언되는 함수는 숫자 리스트의 평균을 반환하는 작업을 수행한다.
float average (int n_args, ...);
가변 인자의 작동 방식 때문에 전달인자에서 가변 길이의 요소 숫자를 지정해야 한다. 여기 average 함수에서는 n_args라는 전달인자를 사용한다. printf 함수에서는 당신에게 입력한 첫 번째 문자열에서 지정하는 형식의 코드를 사용한다.
이제 함수가 가변 인자를 사용할 수 있게 선언하였으니, 함수에 실제 작업을 수행하는 코드를 작성한다. average 함수에 관해 가변 인자 리스트에 저장된 숫자에 접근하기 위해서 먼저 리스트 자체에 대해 변수를 선언한다.
va_list myList;
va_list 타입은 <stdarg.h>에서 선언되었으며 기본적으로 리스트를 추적할 수 있다. 하지만 myList를 실제로 사용하려면 먼저 값을 할당해야 한다. 값을 할당하기 위해서는 va_start 타입을 호출해야 하는데, 이 또한 <stdarg.h>에 정의되어 있다. 사용 중인 va_list 변수와 엑세스 중인 변수의 기본 데이터 유형(e.g. int, char)을 va_start 인자에게 제공해야 한다.
#include <stdarg.h>
float average (int n_args, ...)
{
va_list myList;
va_start (myList, n_args);
va_end (myList);
}
n_args 정수를 가변 인자 리스트에서 빼냄으로써 우리는 숫자의 평균을 구할 수 있게 된다.
#include <stdarg.h>
float average (int n_args, ...)
{
va_list myList;
va_start (myList, n_args);
int numbersAdded = 0;
int sum = 0;
while (numbersAdded < n_args) {
int number = va_arg (myList, int); // Get next number from list
sum += number;
numbersAdded += 1;
}
va_end (myList);
float avg = (float)(sum) / (float)(numbersAdded); // Find the average
return avg;
}
(2, 10, 20)을 입력하여 10과 20의 평균, 즉 15를 얻는다.