
1. 파일의 분할
지금까지 구현한 예제들은 크기에 상관없이 하나의 파일로 작성되었다.
실제로 이런 형태로 프로그램을 구현할 수 있다.
하지만 이런 경우 프로그램의 크기가 커지면 관리하기가 어려워진다는 문제점이 발생한다.
따라서 현명한 프로그래머들은 여러 개의 파일을 만들어서 서로 연관 있는 함수와 변수들을 구분해서 담는다.
두 개의 서랍장이 있다.
하나는 수납공간이 통으로 된 서랍장이고, 다른 하나는 크고 작은 여러 개의 공간으로 수납공간이 나눠진 서랍장이다.
어떠한 서랍장이 물건 관리가 용이하겠는가?
공간이 여러 개이면 물건을 용도 및 특성 별로 나눠서 저장할 수 있고, 그러면 물건도 쉽게 찾을 수 있으니, 당연히 여러 개의 공간으로 나눠진 서랍장이 물건관리가 용이하다.
그런데 파일을 나누는 이유도 이와 별반 차이가 없다.
파일을 나눠서 각각의 파일에, 용도 및 특성 별로 함수와 변수를 나눠서 저장하면 소스코드의 관리가 용이해진다.
그럼 파일을 나누는 방법에 대한 고민을 시작해보자.
그리고 이를 위해서 먼저 다음 프로그램을 대상으로 파일을 나눠보기로 하겠다.
#include <stdio.h>
int num=0;
void Increment(void)
{
num++;
}
int GetNum(void)
{
return num;
}
int main(void)
{
printf("%d", GetNum());
Increment();
printf("%d", GetNum());
return 0;
}
이 파일을 다음과 같이 총 세 개의 파일로 나눠서 저장한다고 가정해 보자.
num.c
int num=0;
func.c
void Increment(void)
{
num++;
}
int GetNum(void)
{
return num;
}
main.c
#include <stdio.h>
int main(void)
{
printf("%d", GetNum());
Increment();
printf("%d", GetNum());
return 0;
}
안타깝게도 위와 같은 형태로 파일을 나누면 컴파일 시 에러가 발생하는데, 에러의 발생 이유는 컴파일러의 다음과 같은 특성 때문이다.
"컴파일러는 파일 단위로 컴파일을 진행합니다."
쉽게 말해서 컴파일러는 다른 파일의 정보를 참조하여 컴파일을 진행하지 않는다.
때문에 위 func.c를 컴파일 하면 다음과 같은 내용의 불평을 한다.
"변수 num이 도대체 어디에 선언된 거야!"
그리고 마지막 main.c를 컴파일 하면서도 다음과 같은 내용의 불평을 한다.
"Increment 함수는 정의된 적도 없잖아!"
물론 우리는 변수 num이 어느 파일에 있고 Increment 함수가 어디에 정의되어 있는 지 안다.
그러나 컴파일러는 이를 인식하지 못한다.
앞서 컴파일 했더라도 인식하지 못한다.
컴파일러는 각 파일 안에서만 변수 num의 선언을 찾고, Increment 함수의 정의를 찾다가 에러 메시지를 출력할 뿐이다.
그렇기 때문에 파일을 분할해서 컴파일 하기 위해서는 컴파일러에게 다음과 같은 내용의 메세지를 전달해야 한다.
"num은 외부 파일에 int형으로 선언된 변수야!"
"Increment는 반환형과 매개변수의 형이 void인 함수인데, 외부파일에 정의되어 있어!"
이 중에서 첫 번째 메시지는 func.c를 컴파일 할 때 필요하다.
따라서 func.c에는 다음의 선언이 삽입되어야 하는데, 여기서 extern은 int형 변수 num이 외부에 선언되었음을 컴파일러에게 알릴 때 사용되는 키워드이다.
extern int num; // int형 변수 num이 외부에 선언되어 있다.
즉 위의 선언은 변수 num을 할당하는 선언이 아니다.
num의 자료형이 무엇이고, 어디에 선언되어 있는지를 컴파일러에게 알려주는 메세지일 뿐이다.
그리고 두 번째 메시지는 main.c에 필요하다.
따라서 main.c에는 다음의 선언이 삽입되어야 한다.
extern void Increment(void); // void Increment(void) 함수가 외부에 정의되어 있다.
참고로 함수가 외부에 정의되어 있음을 알릴 때에는 extern 선언을 생략할 수 있다.
즉 다음과 같이 선언해도 동일한 메시지가 컴파일러에게 전달된다.
void Increment(void);
참고로 컴파일러에게는 extern 선언을 통해서 함수 또는 변수가 외부에 선언 및 정의되어 있다는 사실만 알리면 된다.
구체적으로 어느 파일에 선언 및 정의되어있는지 까지는 알리지 않아도 된다.
아래는 위의 방법으로 완성된 전체 소스코드이다.
num.c
int num=0;
func.c
extern int num;
void Increment(void)
{
num++;
}
int GetNum(void)
{
return num;
}
main.c
#include <stdio.h>
extern void Increment(void);
extern int GetNum(void);
int main(void)
{
printf("%d", GetNum());
Increment();
printf("%d", GetNum());
return 0;
}
2. static에 대한 고찰
이미 오래 전에 'static 지역변수'에 대해서 설명을 했는데, 이를 이어서 'static 전역변수'에 대해 설명하고자 한다.
전역 변수의 static 선언은 다음의 의미를 담고 있다.
"이 변수는 외부 파일에서의 접근을 허용하지 않는다."
이를 다르게 표현하면 다음과 같다.
"이 변수의 접근범위는 파일 내부로 제한한다."
따라서 num.c에 선언된 변수 num을 다음과 같이 선언하면 func.c에서는 변수 num에 접근할 수가 없어서 컴파일 오류가 발생한다.
static int num=10;
3. 헤더파일의 디자인과 활용
먼저 #include 지시자의 의미를 이해하자.
그러면 헤더파일을 이해할 수 있을 뿐만 아니라, 헤더파일에 무엇을 담아야 할지도 알 수 있게 되니 말이다.
이를 위해서 다음 예제를 제시하겠다.
header1.h
{
puts("Hello World");
header2.h
return 0;
}
main.c
#include <stdio.h>
int main(void)
#include "header1.h"
#include "header2.h"
언뜻 보면 이것이 정상적인 프로그램인가 하는 의심이 든다.
하지만 정상적인 프로그램이 맞다.
일단 이 프로그램의 분석을 위해서 다음 문장의 의미를 설명하겠다.
#include "header1.h"
이는 다음과 같은 메시지를 선행처리기에 전달하는 것이다.
"이 문장의 위치에다가 header1.h에 저장된 내용을 가져다 놓으세요."
유사하게 main.c의 2번째 include 지시자는 다음의 의미를 갖는다.
"이 문장의 위치에다가 header2.h가 저장된 내용을 가져다 놓으세요."
그럼 가져다 놓아보자.
그러면 정상적인 프로그램임을 알 수 있다.
#include <stdio.h>
int main(void)
{
puts("Hello World");
return 0;
}
이처럼 #include 지시자는 그 이름이 의미하듯이 파일의 내용을 단순히 포함시키는 용도로 사용된다.
그 이상 그 이하도 아닌 단순한 '포함'일 뿐이다.
헤더파일을 포함하는 방식에는 두 가지가 있다.
#include <헤더파일 이름>
#include "헤더파일 이름"
이 둘의 유일한 차이점은 포함시킬 헤더파일의 기본 경로인데, 첫 번째 방식을 사용하면 표준 헤더파일(C의 표준에서 정의하고 있는, 기본적으로 제공되는 헤더파일)이 저장되어 있는 디렉터리에서 파일을 찾게 된다.
때문에 이 방식은 stdio.h, stdlib.h 그리고 string.h와 같은 표준 헤더파일을 포함시킬 경우에 사용된다.
반면 두 번째 방식을 사용하면, 이 문장을 포함하는 소스파일이 저장된 디렉터리에서 헤더파일을 찾는다.
때문에 프로그래머가 정의하는 헤더파일을 포함시킬 때 사용하는 방식이다.
그리고 이 방식을 사용하면 다음과 같이 헤더파일의 이름뿐만 아니라, 드라이브 명과 디렉터리 경로를 포함하는 '절대경로(완전경로)'를 명시해서 헤더파일을 지정할 수 있다.
#include "C:\Power\MyProject\header.h"
그렇다면 헤더파일에 무엇을 담으면 좋을까?
사실 이에 대한 힌트는 앞서 예제를 통해서 충분히 제시되었다.
기본적으로 헤더파일에는 다음과 같은 유형의 선언을 담게 된다.
extern int num;
extern int GetNum(void);
외부에 선언된 변수에 접근하거나 외부에 정의된 함수를 호출하기 위한 선언들인데, 이들은 둘 이상의 소스파일로 이뤄진 프로그램에서 당연히 삽입될 수 밖에 없는 유형의 선언들이다.
그런데 필요할 때마다 매번 삽입하는 것은 번거로운 일이다.
따라서 이 선언들을 헤더파일에 모아두고 필요할 때마다 헤더파일을 포함시키는 방법을 선택한다.
우리가 printf나 scanf를 사용할 때 <stdio.h>를, strcpy를 사용할 때 <string.h>를 include 하는 것처럼, 우리가 main 함수에서 사용할 함수들을 헤더 파일에 담으면 된다고 이해하면 좋다.
basicArith.h
#define PI 3.1415
double Add(double num1, double num2);
double Min(double num1, double num2);
double Mul(double num1, double num2);
double Div(double num1, double num2);
basicArith.c
double Add(double num1, double num2)
{
return num1+num2;
}
double Min(double num1, double num2)
{
return num1-num2;
}
double Mul(double num1, double num2)
{
return num1*num2;
}
double Div(double num1, double num2)
{
return num1/num2;
}
basicArith.c에는 사칙연산의 기능을 제공하는 함수들이 정의되어 있다.
그리고 이 함수들의 선언을 basicArith.h에 모아두었으므로, basicArith.c에 정의된 함수의 호출을 위해서는 헤더파일 basicArith.h를 포함시켜야 한다.
areaArith.h
double TriangleArea(double base, double height);
double CircleArea(double rad);
areaArith.c
#include "basicArith.h"
double TriangleArea(double base, double height)
{
return Div(Mul(base, height), 2);
}
double CircleArea(double rad)
{
return Mul(Mul(rad, rad), PI);
}
areaArith.c에는 면적을 구하는 함수들이 정의되어 있다.
그런데 이 함수들은 basicArith.c에 정의된 함수를 호출하기 때문에 헤더파일 basicArith.h를 포함시켜야 한다.
roundArith.h
double RectangleRound(double base, double height);
double SquareRound(double side);
roundArith.c
#include "basicArith.h"
double RectangleRound(double base, double height)
{
return Mul(Add(base, height), 2);
}
double SquareRound(double side)
{
return Mul(side, 4);
}
roundArith.c에는 둘레를 구하는 함수들이 정의되어 있다.
이 함수들 역시 basicArith.c에 정의된 함수를 호출하기 때문에 헤더파일 basicArith.h를 포함시켜야 한다.
잠시 여기서 헤더파일의 유용함을 관찰해보자.
만약에 헤더파일 basicArith.h가 존재하지 않았다면, 소스파일 areaArith.c와 roundArith.c에서 호출하고 있는 함수의 선언을 각각의 소스파일에 추가해야만 한다.
즉 번거로운 과정을 거쳐야만 하는 것이다.
그러나 헤더파일을 만들었기 때문에 한줄의 #include문으로 모든 것이 해결되었다.
이것이 바로 헤더파일의 유용함이다.
이제 위의 파일들을 테스트하기 위한 main 함수를 소개하겠다.
main.c
#include <stdio.h>
#include "areaArith.h"
#include "roundArith.h"
int main(void)
{
printf("삼각형 넓이(밑변 4, 높이 2): %g \n",
TriangleArea(4,2));
printf("원 넓이(반지름 3): %g \n",
CircleArea(3));
printf("직사각형 둘레(밑변 2.5, 높이 5.2): %g \n",
RectangleRound(2.5, 5.2));
printf("정사각형 둘레(변의 길이 3): %g \n",
SquareRound(3));
return 0;
}
이전에 구조체를 설명할 때도 간단히 언급했듯이 구조체는 프로그램 개발에서 빠질 수 없는 중요한 요소이다.
그렇다면 구조체의 선언(typedef 선언) 및 정의는 어디에 두는 것이 정답일까?
소스파일일까 아니면 헤더파일일까?
두 개의 파일로 이뤄진 다음 예제를 참조하여 이 부분에 대한 답을 대략적으로나마 내려보자
intdiv.c
typedef struct div
{
int quotient; // 몫
int remainder; // 나머지
} Div;
Div IntDiv(int num1, int num2)
{
Div dval;
dval.quotient=num1/num2;
dval.remainder=num1%num2;
return dval;
}
main.c
#include <stdio.h>
typedef struct div
{
int quotient;
int remainder;
} Div;
extern Div IntDiv(int num1, int num2);
int main(void)
{
Div val = IntDiv(5,2);
printf("%d, %d", val.quotient, val.remainder);
return 0;
}
위 예제를 분석하는 과정에서 다음 사실에 놀라지 않을 수 없다.
"구조체 Div의 선언 및 정의가 두 번씩이나 삽입되었네요?"
처음 보면 상당히 이상하게 보이지만 앞서 설명한 다음 사실을 기억하면 전혀 이상하게 느껴지지 않을 것이다.
"컴파일러는 파일 단위로 컴파일을 진행합니다."
컴파일러는 다른 파일의 정보를 참조하여 컴파일을 진행하지 않는다고 하였다.
때문에 구조체 Div에 대한 선언 및 정의는 Div를 필요로 하는 모든 파일에 존재해야 한다.
그러나 우리는 헤더파일을 만들어서 Div의 선언 및 정의가 프로그램 내에서 하나만 존재하도록 개선시킬 수 있다.
아니 반드시 개선시켜야 한다.
동일한 구조체의 정의가 두 군데 이상 존재하면 구조체의 수정 및 확장에 불편함이 따르기 때문이다.
stdiv.h
typedef struct div
{
int quotient;
int remainder;
} Div;
intdiv2.c
#include "stdiv.h"
Div IntDiv(int num1, int num2)
{
Div dval;
dval.quotient=num1/num2;
dval.remainder=num1%num2;
return dval;
}
main.c
#include <stdio.h>
#include "stdiv.h"
extern Div IntDiv(int num1, int num2);
int main(void)
{
Div val = IntDiv(5,2);
printf("%d, %d", val.quotient, val.remainder);
return 0;
}
위 예제에서 보이듯이 구조체의 선언 및 정의는 헤더파일에 삽입하는 것이 좋다.
일단 구조체의 선언 및 정의는 헤더파일에 삽입하는 것이 좋다는 결론이 내려졌다.
그런데 이는 자칫 컴파일 에러의 원인으로 이어질 수 있어서 주의를 해야 한다.
총 네 개의 파일로 이뤄진 다음 예제를 통해서 알아보자.
stdiv.h
typedef struct div
{
int quotient;
int remainder;
} Div;
intdiv3.c
#include "stdiv.h"
Div IntDiv(int num1, int num2)
{
Div dval;
dval.quotient=num1/num2;
dval.remainder=num1%num2;
return dval;
}
intdiv3.h
#include "stdiv.h"
extern Div IntDiv(int num1, int num2);
main.c
#include <stdio.h>
#include "stdiv.h"
#include "intdiv3.h"
int main(void)
{
Div val = IntDiv(5, 2);
printf("%d, %d", val.quotient, val.remainder);
return 0;
}
이 예제는 파일을 하나씩만 놓고 보면 문제될 것이 없어 보인다.
그러나 이들을 묶어놓고 보면 문제가 발생한다.
main.c가 포함하는 헤더파일의 종류와 수를 관찰해 보자.
일단 다음 문장에 의해서 stdiv.h을 한 번 포함한다.
#include "stdiv.h"
그리고 다음 문장에 의해서 stdiv.h를 한 번 더 포함한다.
왜냐하면 헤더파일 intdiv3.h가 stdiv.h를 포함하고 있기 때문이다.
#include "intdiv3.h"
결과적으로 main.c에서는 구조체 Div가 두 번 정의된 형태가 되어 컴파일 에러가 발생한다.
그렇다면 이 문제를 어떻게 해결해야 할까?
가장 간단한 방법은 main.c에서 stdiv.h를 포함하는 #include문을 삭제하는 것이다.
어차피 헤더파일 intdiv3.h를 통해서 stdiv.h를 포함하게 되니 삭제를 해도 문제되지 않는다.
그러나 이는 이 상황에서의 해결책일 뿐 일반적인 해결책은 되지 못한다.
프로그램이 복잡해질수록 소스파일과 헤더파일은 많아지고, 더불어 헤더파일의 포함관계도 복잡해진다.
때문에 헤더파일을 한 번만 포함시키도록 주의해서 프로그래밍을 하는 것은 매우 거슬리는 일이 되어버린다.
따라서 보다 근본적인 대책이 필요하다.
헤더파일의 중복삽입을 고민하지 않아도 되는 아주 근본적인 대책 말이다.
헤더파일의 중복삽입에 대한 해결책은 앞서 배운 '조건부 컴파일을 위한 매크로'에서 찾을 수 있다.
stdiv2.h
#ifndef __STDIV2_H__
#define __STDIV2_H__
typedef struct div
{
int quotient;
int remainder;
} Div;
#endif
위의 파일은 앞서 중복삽입으로 인해 문제가 되었던 헤더파일이다.
그런데 이 파일의 1, 2행 그리고 마지막 행을 통해서 중복삽입을 막고 있다.
이 파일을 처음 포함하는 소스파일은 __STDIV2_H__라는 이름의 매크로가 정의되지 않은 상태이므로 2~8행까지를 포함하게 된다.
때문에 2행에 의해서 매크로 __STDIV2_H__가 정의되고 이어서 4~8행에 의해서 구조체 Div가 정의된다.
그리고 이후에 이 파일을 다시 포함하는 경우에는 매크로 __STDIV2_H__가 이미 정의된 상태이므로 1행과 10행에 의해서 그 사이에 있는 모든 내용이 포함되지 않는다.
결국 구조체 Div는 소스파일당 하나씩만 정의가 된다.
intdiv4.h
#ifndef __INTDIV4_H__
#define __INTDIV4_H__
#include "stdiv2.h"
extern Div IntDiv(int num1, int num2);
#endif
위의 파일은 중복삽입으로 인한 문제를 일으켰던 헤더파일은 아니다.
하지만 헤더파일에 존재하는 내용은 이렇듯 #ifndef~#endif를 이용해서 중복삽입의 문제를 미연에 방지하는 것이 좋다.
intdiv4.c
#include "stdiv2.h"
Div IntDiv(int num1, int num2)
{
Div dval;
dval.quotient=num1/num2;
dval.remainder=num1%num2;
return dval;
}
main.c
#include <stdio.h>
#include "stdiv2.h"
#include "intdiv4.h"
int main(void)
{
Div val = IntDiv(5, 2);
printf("%d, %d", val.quotient, val.remainder);
return 0;
}
위의 main.c에서는 2행과 3행에 의해서 stdiv2.h를 두 번 포함하려 든다.
하지만 stdiv2.h에 삽입된 매크로 지시자 #ifndef~#endif에 의해서 중복삽입으로 인한 문제는 발생하지 않는다.