6-2.헤더 파일

6-2-가.함수의 원형

함수의 원형을 이해하기 위해서는 C 컴파일러의 컴파일 방식에 대해 알아야 한다. 프로그래밍 언어는 해석 방식에 따라 인터프리터 방식과 컴파일 방식으로 나누어지는데 컴파일 방식이 훨씬 더 성능이 좋기 때문에 대부분의 언어가 컴파일 방식을 사용한다. C언어도 물론 컴파일 방식을 사용한다.

컴파일 방식은 소스를 읽어 기계어로 한꺼번에 번역하는 방식인데 번역을 몇 번에 나누어 하느냐에 따라 1패스, 2패스 등으로 구분된다. 한 번 죽 읽어서 번역을 다 해 내면 1패스 (one pass)방식이라고 하며 한 번 읽어서 컴파일 준비를 한 후 다시 읽으면서 기계어 코드로 바꾸는 방식을 2패스 방식이라고 한다. 2패스 방식의 대표적인 예가 어셈블러인데 소스상의 위치를 나타내는 레이블을 실제 번지로 바꾸기 위해 처음부터 끝까지 소스를 읽어 레이블의 번지를 미리 파악한 후 다시 처음부터 읽으면서 기계어 코드로 번역한다. 어셈블러는 이런 식으로 소스를 두 번 읽지 않으면 컴파일이 불가능하다.

3패스 방식을 채택하는 언어도 있고 디스 어셈블러들은 5패스 방식까지도 사용한다. 언어가 복잡해질수록 패스 수가 늘어나며 사용하는 메모리는 많아지고 컴파일 속도는 떨어진다. 초기의 C 컴파일러들은 컴파일 속도를 높이기 위해 통상 1패스 방식을 많이 채택했었다. C는 다른 언어보다 문법 구조가 복잡해서 컴파일 속도가 느린 편이라 패스를 여러 번 할 수가 없었다. 그래서 C 소스는 컴파일러에 의해 읽히는 족족 기계어로 번역된다.

물론 최근의 C컴파일러들은 1패스가 아닌 것들도 있지만 초기의 컴파일러들이 1패스 방식으로 작성되었기 때문에 C표준은 이런 컴파일러를 위해 한 번에 소스를 읽을 수 있는 장치를 마련할 필요가 있었다. 이러한 C의 1패스 방식 때문에 함수의 원형이라는 것이 필요하다. 원형(ProtoType)이란 함수에 대한 정확한 정보라는 뜻이며 리턴 타입, 함수 이름, 인수 리스트 등의 정보들로 구성된다. 원형이라는 것이 왜 필요한지 앞에서 만들었던 Max 예제를 다음과 같이 다시 작성해 보자.

 

: MaxFunc2

#include <Turboc.h>

 

void main()

{

     int a,b,m;

 

     printf("두 수를 입력하세요 : ");

     scanf("%d%d",&a,&b);

     m=Max(a,b);

     printf("두 수 중 큰 값은 %d입니다.\n",m);

}

 

int Max(int a, int b)

{

     if (a > b) {

          return a;

     } else {

          return b;

     }

}

 

원래 예제에 비해 main, Max 함수의 순서가 바뀌었을 뿐 코드는 동일하다. 원래 예제에서는 Max 함수를 먼저 정의하고 main 함수가 그 뒤에 있었으나 이번에는 반대로 되어 있다. 이 예제를 컴파일하면 다음 두가지 에러가 발생한다.

 

CExam.cpp(9) : error C2065: 'Max' : undeclared identifier

CExam.cpp(14) : error C2373: 'Max' : redefinition; different type modifiers

 

첫 번째 에러는 main 함수의 중간쯤에 있는 Max라는 명칭이 정의되지 않았다는 뜻이다. 1패스 방식으로 소스를 순서대로 번역하므로 Max 함수보다는 main 함수를 먼저 번역하는데 main 함수의 m=Max(a,b)를 번역하는 시점에서는 Max가 함수인지, 변수인지 조차도 아직 파악이 안된 상태이다. 그래서 Max가 무엇인지 모르겠다는 에러 메시지가 출력되는 것이다.

컴파일러가 아직 이해하지 못하는 함수 호출문을 만났으므로 이 상태에서는 컴파일할 수 없으며 Max라는 명칭에 대해 에러로 기록할 것이다. 두 번째 에러 메시지는 앞에서 이해하지 못했던 Max라는 명칭을 다시 함수로 정의하고 있으므로 중복 정의했다는 뜻이다. 컴파일러는 처음 보는 명칭에 대해 내부적인 디폴트를 가정하는데 이 디폴트와 다르게 선언되었다는 불만 표시이다.

이 에러 메시지들이 출력되는 원인은 C 컴파일러가 1패스 방식으로 소스를 해석하기 때문이다. 최초 main에서 Max라는 명칭을 만났을 때 소스의 뒤쪽을 미리 읽어 두었다면 Max가 함수라는 것을 알 수 있지만 순서대로 딱 한 번만 읽기 때문에 Max의 실체를 파악하지 못하는 것이다. 이 현상을 좀 더 쉽게 이해하려면 다음 예를 생각해 보면 된다.

 

i=3;

int i;

 

선언과 대입의 순서가 바뀌어 있으므로 에러가 될 것임은 너무 당연하다. 순서를 바꾸어야만 제대로 컴파일될 것이다. 물론 모르는 명칭이 나왔을 때 뒤쪽을 먼저 읽어 보도록 한다면 이런 코드를 제대로 해석하는 컴파일러를 만들 수도 있다. 하지만 이런 컴파일러를 만든다면 복잡해지고 컴파일 속도도 느려질 것이며 예상치 못한 부작용도 생길 수 있으므로 아예 깔끔하게 에러로 처리하는 것이다. 문법은 편리한 것보다는 명료한 것이 훨씬 더 바람직하다.

이 문제를 해결하는 방법은 아주 간단하다. 원래 작성했던 예제처럼 함수의 순서를 바꾸면 된다. main보다 Max를 먼저 정의하고 main에서 Max를 호출한다면 컴파일러가 Max에 대한 정의를 먼저 보게 되므로 아무 문제가 없다. main에서 A를 호출하고 A가 C, D를 호출하고 C가 B를 호출한다면 B-C-D-A-main 순으로 배치하면 일단 문제를 해결할 수는 있다. 호출되는 함수가 항상 먼저 나오면 된다.

그러나 이런 단순한 방법으로는 복잡한 프로그램을 작성하기에 한계가 있다. 왜냐하면 함수의 수가 많아지고 함수끼리 복잡하게 호출해댈 때는 순서를 정하기 무척 어려워지기 때문이다. 함수가 100개만 되어도 순서가 보통 복잡해지는 것이 아니다. 또한 상호 호출이라는 특별한 기법을 사용할 경우도 있기 때문에 함수의 순서를 조정하는 것은 일반적인 해결책이 되지 못한다.

원칙적으로 함수는 사용되기 전에 미리 그 형태를 컴파일러가 알 수 있도록 해야 하는데 그 방법이 바로 원형(ProtoType)을 선언하는 것이다. 함수의 원형을 미리 선언해 두면 본체는 뒤쪽에 있더라도 함수 호출부에서 이 명칭이 함수이고 어떤 인수를 요구한다는 것을 미리 알 수 있게 된다. 이렇게 함수의 정보를 미리 컴파일러에게 알려 주는 것을 "원형을 선언한다"라고 한다. 위 예제를 원형을 선언하여 수정해 보면 다음과 같이 된다.

 

: MaxFunc3

#include <Turboc.h>

 

int Max(int a, int b);               // 원형 선언

 

void main()

{

     int a,b,m;

 

     printf("두 수를 입력하세요 : ");

     scanf("%d%d",&a,&b);

     m=Max(a,b);               // 이미 원형이 선언되었으므로 Max가 함수임을 알 수 있다.

     printf("두 수 중 큰 값은 %d입니다.\n",m);

}

 

int Max(int a, int b)                // 본체는 뒤쪽에 있다.

{

     if (a > b) {

          return a;

     } else {

          return b;

     }

}

 

Max 함수의 본체를 정의하기 전에 main 함수에서 Max를 호출하고 있지만 main 함수 이전에 Max의 원형이 선언되어 있으므로 컴파일러는 Max가 함수라는 것과 어떤 타입의 인수를 요구하는지를 미리 파악할 수 있다. 요약하자면 원형 선언이 필요한 이유는 C 컴파일러가 1패스 방식을 사용하며 딱 한 번 읽어서 번역을 하기 때문에 뒷부분에 나올 함수에 대한 정보를 미리 제공해야 하기 때문이다.