[ C 언어 스터디 ]


자료출처 : 핵심 길잡이 C 프로그래밍 언어
도서출판 : 성안당

7. 함수 ( Functions )

C 언어는 이미 앞에서 사용한 main ( ), printf ( ), scanf ( ), 등의 함수를 사용하는 것
에서 알 수 있듯이 기본적으로 함수의 조합으로 이루어져 있다. 다른 컴퓨터 언어와는
달리 모든 C 프로그램 main ( ) 이라는 특별한 함수를 가지며, main ( ) 함수에서 다른
함수들을 호출 ( calling ) 하여 사용하게 되어 있다.

★ 1. 함수의 기본 개념 ★

함수는 어떤 특정한 작업을 수행하도록 만들어진 독립적인 하나의 단위 프로그램을
의미한다. C 언어의 함수는 다른 언어의 서브루틴 ( subroutine ) 또는 프로시저
( procedure ) 와 유사한 개념이라 할 수 있다. 다시 말해 C 언어의 함수를 모듈
( module ) 이라고도 부르며 서로 영향을 주지 않는 모듈들의 조합으로써 프로그램이 작
성된다. 따라서 하나 하나의 함수를 각각의 실행 단위로 볼 수 있으며, 프로그래밍은
물론 에러의 검출과 수정까지도 모듈 단위로 처리된다.

또한, C 언어의 특징 중에 하나는 간단한 기능만 C 컴파일러에 의해 제공되고 입출
력 기능을 비롯한 대다수의 기능들이 함수에 의해 제공된다는 것이다.

C 언어 프로그램은 하나 이상의 함수들로 구성되며, 그들 중에 main ( ) 함수는 반
드시 존재해야 한다. 프로그램은 main ( ) 함수부터 수행이 시작되며, 실행 순서에 따라
필요한 경우 다른 함수들을 호출 ( calling ) 하면서 프로그램이 수행된다. 즉 main ( ) 함
수가 프로그램의 시작점이라는 것을 제외하고는 프로그램을 구성하는 모든 함수간에는
특정 관계와 우선 순위가 존재하지 않는다.

모든 함수는 서로 독립적이고 동등한 관계를 가지므로 서로간에 호출이 가능하다.
경우에 따라서는 프로그램 내에서 정의하지 않은 함수들, 예를 들어 지금까지 많이 사
용한 printf( ), scanf ( ) 함수와 같은 미리 정의된 라이브러리 함수 ( library function ) 를
호출할 수도 있다.

C 프로그램을 작성할 때 함수를 효율적으로 사용하면 다음과 같은 몇 가지 이점을
얻을 수 있다.

1. 프로그램 내에서 특정 부분을 여러 번 반복적으로 실행해야 하는 경우 반복되는
특정 작업의 내용을 함수로 만들어 필요할 때마다 이를 호출하여 사용하면 편리
하고, 또한 코딩 ( coding )의 반복을 피할 수 있다.

2. 모듈화에 의한 구조적 프로그램 ( structured program ) 방식을 이용하기 때문에
프로그램의 수정과 편집이 매우 용이하다.

3. 잘 만들어진 함수를 라이브러리 안에 저장시킴으로서 다른 응용프로그램에서도
재사용할 수 있게 되어 프로그램의 작성 효율을 높일 수 있다.

4. COBOL 이나 FORTRAN 등 다른 언어로 프로그램을 작성하다가 그 언어들이
제공되지 않는 난해한 부분이 있을 경우에는 그 부분을 C 언어로 만들어서 처리
할 수 있어 그만큼 능률적이다.

5. C 언어는 간단한 기능만 C 컴파일러에 의해 제공되고 대다수 기능들은 함수로
제공되기 때문에, 메모리 내에 C 컴파일러만 적재되고 나머지 기능들은 함수로
만들어져 디스크에 별도의 파일로 저정되어 있다가 필요한 함수만 메모리에 적
재하면 되기 때문에 적은 메모리를 가지고도 대용량 프로그램을 작성할 수 있다.

★ 함수의 정의 ★

C 언어의 함수는 크게 두 가지 종류로 나뉘어진다. 하나는 시스템 내에 이미 정의되
어 있는 함수로 이들은 정의 및 선언 과정을 별도로 거치지 않는다. 이러한 함
수들을 "시스템 정의 함수" 또는 "표준 함수 ( standard function )"라 한다. 그리고 나
머지 하나는 사용자가 용도에 맞게 작성하여 사용하는 사용자 정의 함수( user defined function )"이다.

사용자 정의 함수의 일반 형식은 다음과 같다.

형 식

[ 데이터형 ] 함수명 ( [ 가인수 1, 가인수 2, ......, 가인수 N ] )
[ 가인수 선언 ; ]
{

[ 함수 또는 함수의 내부변수 선언 ; ]
[ 명령문들 ; ]

}


단 [ ]로 묶인 부분은 생략이 가능하다.
프로그램 작성 시 해당 함수를 사용하고자 할 때에는 그 함수명으로 호출하게 된다.
함수명은 사용자가 임의로 부여하여 만들고, 사용할 할 수 있다. 이때 함수명은 변수명을
부여하는 규칙과 동일한 규칙이 적용되며, 함수명도 데이터형을 가진다.

함수에 대한 데이터형의 지정은 변수에 대한 자료형 선언 방법과 동일하다. 자료형
을 생략하는 경우에는 보통 int 형으로 간주한다. 데이터형은 함수가 호출되어 실행된
후 그 결과를 후출한 함수로 되돌려 줄 때의 데이터형을 나타낸다. 만약 데이터형에
void형이 오면 그 함수는 실행한 결과를 호출 함수로 반환하지 않는다는 것을 나타
낸다.

또한 함수 정의의 머리 부분에 정의된 인수를 가인수 ( formal argument 또는 formal
parameter )라 한다. 이는 함수가 호출되었을 때 전달되는 값들의 저장 장소 역할을 한
다. 이때 호출 프로그램에 정의된 인수는 실인수 ( actual argument )라 하여 가인수와
구분된다.

함수를 정의할 때 그 함수가 다른 함수로부터 값을 전달받아서 실행하는 함수인 경
우에는 반드시 괄호 안에 인수들을 나열하고, 함수의 머리 부분에 그 가인수들을 선언
해 주어야 한다. 그러나 다른 함수로부터 제어권만 넘겨받는 함수인 경우에는 가인수를
사용하지 않는다. 이 경우에도 괄호는 반드시 있어야 한다. 함수의 몸체 부분은 함수를
실행하기 위한 변수들의 선언과 명령문들로 이루어져 있다.

다음 예는 호출 함수로부터 2 개의 정수를 전달받아서 제곱을 계산하여 되돌리는 함
수를 정의한 것이다.

int x , y ;
int square ( x , y )
{

int z ;
z = x * y ;
return z ;

}

★ 함수의 사용 ★

< 1. 함수의 선언 >

어떤 함수 내에서 다른 곳에 정의된 함수를 호출하기 위해서는 먼저 그 함수를 호출
함수 내에 선언해 주어야 한다. 그러나 피호출 함수 ( called function )가 호출 함수
( calling function ) 보다 앞에 정의되어 있으면 함수 선언을 생략할 수 있다. 함수의 선
어 형식은 다음과 같다.

형 식
[ 데이터형 ] 함수명 ( ) ;

데이터형이 int 인 경우에는 함수 선어 시 데이터형을 생략할 수 있다. 함수 선언은
함수 내부에서 할 수도 있고, 함수 외부에서도 할 수 있다. 우리가 앞에서 printf ( ) 함
수나 scanf ( ) 함수를 선언하지 않고 사용할 수 있었던 것은 선행처리기 부분에
# include < stdio.h >가 선언되어 있기 때문이다.

2 개의 정수를 입력받아 곱을 계산하는 프로그램이다. 실행 결과를 나타내시오

▶ 프로그램

# include < stdio.h >

void main ( )
{

int x , y , z ;
int square ( ) ; // 함수의 선언

scanf ( " %d   %d " , &x , &y ) ;
z = square ( x , y ) ;  // 함수의 호출
printf ( " %d * %d = %d 입니다. \n " , x , y , z ) ;

}

int square ( int a , int b )
{

return ( a * b ) ;

}
▶ 실행 결과
2   3   Enter Button Push
2 * 3 = 6 입니다.


위의 프로그램은 main ( ) 함수에서 함수 square ( )를 호출하기 전에 int square ( ) ;로
함수를 선언하고 있다. 그러나 다음과 같이 함수 square ( )를 먼저 정의하면 main ( ) 함
수 내에 함수 square ( )를 선언하지 않아도 된다.

# include < stdio.h >
int square ( a , b )
int a , b ;
{

return ( a * b ) ;

}
void main ( void )
{

int   x , y , z ;
scanf ( " %d  %d " , &x , &y ) ;
z = square ( x , y ) ;
printf ( " %d   *  %d = %d 입니다. \n " , x , y , z ) ;

}
최근 C 언어에서는 함수를 정의할 때나 함수를 선언할 때 인수들의 데이터형을 괄
호 안에 바로 선언함으로써 함수를 호출할 때 인수들의 데이터형이 서로 일치하지
않으면 컴파일러가 오류를 발생시키도록 하고 있다. 이것을 함수의 원형 ( prototype )
이라 한다.
예를 들어

int  square ( a , b )
int  a , b ;
{

return ( a * b ) ;

}
를 함수 원형의 형태로 정의한다면

int square ( int a , int b )
{

return ( a * b ) ;

}
와 같다. 또한 호출 함수 내에서 위의 square ( ) 함수를 선언할 때

int square ( ) ;

와 같이 인수 없이 선어하였기 때문에 컴파일러가 인수들의 데이터형이 정확하게 사용되
고 있는지 확인할 수 없었다. 그러나 다음과 같이 함수 원형 형태로 선언하며,

int square ( int a , int b ) ;

와 같이 선언되므로 만일

square ( 6.75 , 3 );

과 같이 함수를 호출한다면 인수들의 데이터형이 일치하지 않으므로 컴파일러가 오류
를 발생시킨다.

함수 원형의 형태로 함수를 선언할 때 인수명을 생략하여

int square ( int , int ) ;

와 같이 선언할 수도 있으나 프로그램을 쉽게 이해하기 위해서는 인수명을 써 주는 것
이 좋다.

< 2. 함수의 호출 >

함수는 다른 곳에 정의된 함수나 자기 자신을 호출할 수 있다. 일반적으로 함수는 다
음과 같이 함수명을 사용하여 호출한다.

형 식
함수명 ( [ 실인수 1 , 실인수 2, ....... 실인수 n ] )


C 언어에서 함수들간의 데이터 전달은 인수 ( argument )를 통하여 전달하는 방법과,
함수들이 외부 변수 ( external variable )를 서로 공유함으로써 자동으로 전달하는 방법
이 있다.

인수에는 실인수 ( actual argument )와 가인수 ( formal argument )가 있다. 호출 함수
에서 실인수를 통하여 데이터를 전달하면 피호출 함수에서는 가인수를 통하여 데이터
를 전달받는다. 이와 같이 실인수와 가인수는 함수들간에 데이터를 전달해 주는 매개변
수 ( parameter )이므로 서로 대응되는 인수들의 데이터형과 개수가 일치하여야 한다.

그러나  함수들은 서로 독립적이므로 변수명이 같을 필요는 없다. [ 그림 7 -1 ]은 이들의
관계를 나타내고 있다.

● 호출 함수

# include  < stdio.h >
void main ( )
{

int  a , b , c ;
int  sum ( ) ;          // 함수의 선언
scanf ( " %d %d " , &a , &b ) ;
c = sum ( a , b ) ;      // 함수의 호출
printf ( " %d + %d = %d \n " , a , b , c ) ;

}
● 피호출 함수
int sum ( x , y )    // 함수의 정의
int  x , y      // 가인수 선언
{

return ( x , y ) ;

}

★ 함수의 매개변수 전달 ( Parameter passing ) ★

C 언어에서 인수를 통하여 데이터를 전달하는 방식에는 값에 의한 호출 ( call by value )
방식과 주소에 의한 호출 ( call by reference ) 방식이 있다.

< 1. 값에 의한 호출 ( Call by value ) >

값에 의한 호출 방식은 C 언어에서 가장 보편화된 데이터 전달 방식이다. 이 방식은
호출 함수의 실인수를 평가한 후 그 값을 대응되는 피호출 함수의 가인수에 전달하기
때문에 피호출 함수에서 가인수를 통하여 전달받은 내용을 변경하더라도 실인수의 내
용은 변하지 않는다. 그러므로 함수간의 독립성이 유지되어 부작용 ( side effect )을 방
지할 수 있다. 따라서, 값에 의한 호출 방식이 함수의 독립성과 안전성 측면에서 매우
유리하다.

다음은 call by value 방식으로 함수를 호출하는 프로그램이다. 실행 결과를 나타내시오

▶ 프로그램
# include  < stdio.h >
void main ( )
{

int  a = 10 , b=15 , c ;
int multiply ( int a , int b ) ;
c = multiply ( a , b ) ;
printf ( " %d * %d = %d \n " , a , b , c ) ;

}
int multiply ( int a , int b )
{

int  c ;
c = a * b ;
return ( c ) ;

}
▶ 실행 결과
10 * 15 = 150



다음 프로그램의 실행 결과를 나타내시오

▶ 프로그램
# include  < stdio.h >
void main ( )
{

int  a = 10 , b =15 , c ;
int   sub ( ) ;
c = sub ( a , b ) ;
printf ( " 호출 함수 내의 a 값 = %d \n " , a ) ;
printf ( " %d - %d = %d \n " , a , b, c );

}
int sub ( int a, int b )
{

a = a - b ;
printf ( " 피호출 함수 내의 a 값 = %d \n " , a ) ;
return ( a ) ;

}
▶ 실행 결과
피호출 함수 내의 a 값 = -5
호출 함수 내의 a 값 = 10
10 - 15 = -5



위의 예에서 정의된 실인수 a와 b에는 각각 10과 15가 할당되어 있다. 이때 실인수
a값과 b값은 피호출 함수 내의 가인수 a와 b에 전달된다. 그러나 값에 의한 호출 방식
은 가인수 값을 실인수로 되돌릴 수 없기 때문에 호출 함수와 피호출 함수가 같은 변
수 ( 변수 a )를 사용하더라도 호출 함수의 실인수 a값에 전혀 영향을 주지 않음을 알 수
있다. 피호출 함수에서 가인수 값을 바꾸더라도 호출 함수의 실인수 값을 바꿀 수 없기
때문에 함수간의 독립성을 극대화할 수 있다.

< 2. 주소에 의한 호출 ( Call by reference )>

주소에 의한 호출 방식은 데이터의 주소 ( address )를 인수를 통하여 전달한다. 즉 실인
수에 해당되는 주소를 가인수로 보내는 방법이다. 피호출 함수에서는 전달받은 주소를
이용하여 그 주소에 기억되어 있는 값을 참조할 수 있으며, 그 내용을 변경시킬 수도 있
다. 이러한 경우 호출 함수에도 영향이 미치게 되므로 부작용이 발생할 수도 있다.

▶ 프로그램
# include < stdio.h >
void main ( )
{

int a = 10, b=15, c ;
void multiply ( int *, int *, int * ) ;
multiply ( &a , &b , &c ) ;
printf ( " %d * %d = %d \n " , a , b , c ) ;

}
void multiply ( int *x , int *y , int *z )
{

*z = *x * *y ;

}
▶ 실행 결과
10 * 15 = 150


위의 프로그램은 main ( ) 함수에서 함수 mutiply ( )를 호출할 때 실인수를 통하여 a ,
b, c 의 주소를 전달한다. 또한 함수 multiply ( )의 가인수 x, y, z는 포인터 변수로서 a,
b, c 의 주소를 전달받는다. 그러므로 *z = *x * *y ; 를 실행하면 결국 피호출 함수에
서 호출 함수의 변수인 c 에 값 ( 150 )을 기억시키게 된다. 함수 multiply ( )에 return문이
생략되었음을 알 수 있다.

★ return 문 ★

피호출 함수는 실행을 마치고 호출 함수로 되돌아가기 위해 return 문을 사용한다. 일
반 형식은 다음과 같다.

형 식
return ;
return ( 식 ) ; 또는 return 식 ;

여기서 return ; 문은 피호출 함수에서 함수의 결과 값을 갖지 않고 호출 함수로 제
에권을 넘기지만, return 식 ; 문은 제어권과 함수의 결과 값을 함께 호출 함수로 넘긴
다. 이때 결과 값으로 하나의 값만을 허용하며, 결과 값의 데이터형은 함수를 정의할
때 정의한 함수의 데이터형과 일치하여야 한다.

피호출 함수의 마지막 명령문이 return ; 문인 경우에는 생략이 가능하다. return 문에
의하여 제어권이 호출 함수로 되돌아가면 함수 호출 직후부터 실행을 계속한다.

두 개의 정수형 변수에 기억된 값을 바꾸는 프로그램을 함수를 이용하여 작성하라

▶ 프로그램 : call by value 를 이용한 경우

# include  < stdio.h >

void change ( a, b )
int a , b ;
{

int temp ;
temp = a ;
b = temp ;
return ;

}
void main ( void )
{

int x = 5 , y = 6 ;
printf ( " x = %d   y = %d \n " , x , y ) ;
change ( x , y ) ;
printf ( " x = %d   y = %d \n " , x , y ) ;

}
▶ 실행 결과
x = 5    y = 6
x = 5    y = 6


위의 예제에서 함수 change ( )는 가인수 a , b를 통하여 main ( ) 함수의 x , y값을 전달
받는다. 그리고 함수 내에서 이들 값 즉 a , b의 값을 바꾸어 주고 main ( ) 함수로 되
돌아 간다. 그러나 가인수 a , b의 교환은 실인수 x , y에 영향을 미치지 못하므로 x , y
의 값은 함수를 호출한 후에도 호출하기 전과 변함이 없다.

위 예제를 call by reference를 이용하여 프로그램을 작성하라

▶ 프로그램

# include  < stdio.h >
void change ( a , b )
int *a , *b
{

int temp ;
temp = *a ;
*a = *b ;
*b = temp ;
return ;

}
void main ( void )
{

int  x = 5 , y = 6 ;
printf ( " x = %d   y = %d \n " , x , y ) ;
change ( &x , &y ) ;
printf ( " x = %d  y = %d \n " , x , y ) ;

}
▶ 실행 결과
x = 5 y= 6
x = 6 y= 5


위 예제 함수 change ( )는 가인수 a, b를 통하여 main ( ) 함수의 x, y의 주소
( address )를 전달받는다. 그러므로 a, b는 각각 x 와 y의 주소를 기억하는 포인터 변수
이다. a와 b가 가리키는 주소의 내용은 결국 x와 y의 값을 나타내므로 *a 와 *b 의 값을
바꾸면 x 와 y 의 값이 바뀌게 된다.

★ 함수의 재귀적 호출 ( Recursive call ) ★

함수는 자기 자신을 직접 또는 간접적으로 다시 호출할 수 있는데 이것을 함수의 재
귀적 호출( recursive call ) 또는 되부름( recursion )이라 한다.
함수 재귀적인 호출을 하면 한 번 호출될 때마다 그 안에서 사용하는 변수들이 스
택( stack ) 에 보관되며, return문이 실행될 때마다 스택 내의 변수들이 제거된다. 따라
서 재귀적인 호출이 많으면 스택 오버플로우( stack overflow )가 발생할 수 있다.

다음 프로그램의 실행 결과를 나타내시오
▶ 프로그램
#include < stdio.h>
int factorial ( int n ) ;
void main ( void )
{

int a ;
a = factorial ( 5 ) ;
printf ( " 5! = %d \n " , a ) ;

}
int factorial ( int n )
{

if ( n ==1 )
return ( 1 ) ;
else
return ( n * factorial ( n - 1 ) ) ;

}
▶ 실행 결과
5! = 120

★ void형 함수 ★

모든 함수는 return문을 사용하여 함수의 결과 값을 반환한다. 만약 return문이 없으
면 0 ( zero )을 그 함수의 반환 값으로 한다. 그러나 void형 함수는 결과 값을 반환하지
않는다. 즉, void는 데이터형을 갖지 않는 함수형이며, 이렇게 값을 갖지 않는 함수를
void형으로 선언한다. void형 함수는 반환되는 값이 없으므로 수식 안에 사용될 수 없
다, 또한 void형 함수는 피호출 함수 내에서만 작업을 실행하고, 그 결과가 호출 함수
에 영향을 미칠 필요가 없는 경우에 사용한다.

C 언어에서는 결과 값을 반환하지 않는 함수형을 void로 선언하여 C 컴파일러에게
그 함수가 결과 값을 반환하지 않음을 명확히 밝히지 않으면, return문이 있든지 없든
지 간에 호출 함수로 되돌아갈 때 반드시 어떤 값을 반환한다. 이때 식을 가지고 있지
않은 return문이 수행되면 프로그래며가 알 수 없는 값이 반환된다. 따라서 그러한 값
을 호출한 곳에서 사용해서는 안 된다.

다음 프로그램의 실행 결과를 나타내시오
▶ 프로그램
# include  < stdio.h >
void display ( ) ;
void main ( void )
{

display ( ) ;
printf ( " Hello ! world \n " ) ;
display ( ) ;

}
void display ( )
{

printf ( " ============= \n " ) ;

}
▶ 실행 결과
==============
Hello ! world
==============