Chapter 27. 파일의 분할과 헤더파일의 디자인
C언어로 구현되어 있는 프로그램은 보통 여러 개의 파일로 구성이 된다. 하나의 파일 안에 많은 내용을 담는 것은 관리 측면이나 코드의 의미 전달 측면에서 그리 합리적이지 못하기 때문이다. 따라서 여러 개의 파일을 생성해서 프로그램을 구현하는 것이 일반적이다.
컴파일러가 별도로 컴파일한 파일들을 링커가 링킹 작업을 하여 하나의 실행파일로 만든다.
(파일들을 하나로 묶어주는 역할을 한다.)
27-1 파일의 분할
현명한 프로그래머들은 여러 개의 파일을 만들어서 서로 연관 있는 함수와 변수들을 구분해서 담는다.
파일을 그냥 나둬도 될까요?
파일을 나눠서 각각의 파일에, 용도 및 특성 별로 함수와 변수를 나눠서 저장라면 소스코드의 관리가 용이해진다. 그럼 파일을 나누는 방법에 대한 고민을 시작해보자.
#include
int num = 0;
void Increment(void)
{
num++;
}
int GetNum(void)
{
return num;
}
int main(void)
{
printf("num: %d \n", GetNum());
Increment();
printf("num: %d \n", GetNum());
Increment();
printf("num: %d \n", GetNum());
return 0;
}
위의 코드를 전역변수(num.c), 함수(func.c), 메인함수(main.c) 로 총 세 개의 파일로 나눠서 저장을 한 후 컴파일하면 실행이 안된다.
컴파일러는 파일 단위로 컴파일을 진행하기 때문이다.
외부에 선언 및 정의되었다고 컴파일러에게 알려줘야 합니다!
위의 형태로 파일을 분할해서 컴파일 하기 위해서는 컴파일러에게 메세지를 전달해야 한다.
"num은 외부 파일에 int형으로 선언된 변수야!"
"Increment는 반환형과 매개변수의 형이 void인 함수인데, 외부 파일에 정의되어 있어!"
이 중에서 첫 번째 메세지는 func.c를 컴파일 할 때 필요하다. 따라서 func.c에는 다음의 선언이 삽입 되어야 한다.
extern int num; // int형 변수 num이 외부에 선언되어 있다.
이 선언은 변수 num을 할당하는 선언이 아니라, num의 자료형이 무엇이고, 어디에 선언되어 있는지를 컴파일러에게 알려주는 메시지일 뿐이다.
extern void Increment(void); // void Icrement(void) 함수가 외부에 정의되어 있다.
참고로 함수가 외부에 선언되어 있다고 알릴 때에는 extern 선언을 생략할 수 있다.
void Increment(void);
num.c
#include
int num = 0;
-------------------------------------------------------------------------------------------------
func.c
extern int num;
void Increment(void)
{
num++;
}
int GetNum(void)
{
return num;
}
-------------------------------------------------------------------------------------------------
main.c
extern void Increment(void);
extern int GetNum(void);
int main(void)
{
printf("num: %d \n", GetNum());
Increment();
printf("num: %d \n", GetNum());
Increment();
printf("num: %d \n", GetNum());
return 0;
}
따라서 위의 코드와 같이 소스코드가 수정되어여 한다.
참고로 컴파일러에게는 extern 선언을 통해서 함수 또는 변수가 외부에 선언 및 정의되어 있다는 사실만 알리면 된다. 구체저긍로 어느 파일에 선언 및 정의되어있는지 까지는 알리지 않아도 된다.
다른 파일에서 접근을 못하게 하고 싶다면 static!
이전에 공부했던 'static 지역변수' 와 달리 'static 전역변수'에 대해서 설명한다.
전역변수의 static 선언은 다음의 의미를 담고 있다.
"이 변수는 외부 파일에서의 접근을 허용하지 않는다." " 이 변수의 접근범위는 파일 내부로 제한한다."
따라서 num.c 에 선언된 변수 num을 static int num = 0; 으로 선언하면 func.c에서는 변수 num에 접근할 수가 없어서 컴파일 오류가 발생한다.
이처럼 static 전역변수는 접근의 범위를 파일의 내부로 제한하는 경우에 사용된다.
즉, static 선언은 전역이던지 지역이던지 상관없이 접근의 범위를 제한하는 것이다.
지역변수일경우 -> 그 지역내에서만 접근
전역변수일경우 -> 그 파일 내에서만 접근
27-2 둘 이상의 파일을 컴파일하는 방법과 static에 대한 고찰
파일부터 정리하고 시작합시다!
파일을 생성하고 프로젝트에 추가하는 방법
1. 파일을 먼저 생성해서 코드를 삽입한 다음에 프로젝트에 추가한다.
2. 프로젝트에 파일을 추가한 다음에 코드를 삽입한다.
1번과 같은 경우는 메모장을 포함한 어떠한 편집기를 사용해도 된다. 하지만 이는 일반적으로 컴파일 할 파일이 이미 존재한느 경우에 사용하는 방법이다.
2번은 새로운 파일을 추가해서 코드를 직접 입력하는 경우에 사용하는 방법이다.
1: 이미 만들어진 파일을 프로젝트에 추가하는 방법
파일 탐색기를 열어서 추가할 파일을 끌어다 놓는 형태
솔루션 탐색기의 '소스 파일' 위에서 마우스 오른쪽 버튼을 누르는 방식 (추가 -> 기존 항목)
(참고로 추가할 파일이 헤더파일 이라면 '소스 파일'이 아닌 '헤더 파일'의 위에서 우클릭 후 진행하면 된다.)
2: 프로젝트에 파일을 추가한 다음에 코드를 삽입하여 컴파일하는 방법
위의 두번째 방법에서 '기존 항목'이 아닌 '새 항목' 을 클릭하여 동일하게 진행하면 된다. (코드를 직접 넣어야 함)
함수에도 static 선언을 할 수 있다.
함수에 static 선언을 하면 static 전역변수와 마찬가지로 파일 내에서만 접근이 가능하도록 함수를 제한한다.
static void MinCnt(void)
{
cnt--;
}
함수가 위와 같이 정의되면 extern 선언을 하더라도 다른 파일에서는 접근이 불가능하다.
그리고 이는 코드의 안정성을 높이는 역할을 하는 만큼, 파일 내에서만 호출하기 위해서 정의된 함수라면 이렇듯 static 선언을 추가하여 코드에 안정성을 부여하는 것이 좋다.
27-3 헤더파일의 디자인과 활용
#include 지시자의 의미를 알면 헤더파일을 완전히 이해할 수 있다.
#include 지시자는 그 이름이 의미하듯이 파일의 내용을 단순히 포함시키는 용도로 사용된다.
그 이상도 그 이하도 아닌 단순한 '포함'일 뿐이다.
#include
int main(void)
#include "header1.h"
#include "header2.h"
헤더파일을 include 하는 두가지 방법
#include <헤더파일 이름>
#include "헤더파일 이름"
현재 디렉터리는 변경이 가능하다.
이 둘의 유일한 차이점은 포함시킬 헤더파일의 기본 경로이다.
첫 번째 방식은 표준 헤더파일이 저장되어 있는 디렉터리에서 파일을 찾게 된다.
(stdio.h, stdlib.h, string.h 등 과 같은 헤더파일)
두 번째 방식은 이 문장을 포함하는 소스파일이 저장된 디렉터리에서 헤더파일을 찾는다.
때문에 프로그래머가 정의하는 헤더파일을 포함시킬 때 사용하는 방식이다.
이 방식을 사용하면 헤더파일의 이름뿐만 아니라, 드라이브 명과 디렉터리 경로를 포함하는 '절대경로'를 명시해서 헤더파일을 지정할 수 있다.
#include "C:\CPower\Myproject\header.h" // Windows 상에서의 절대경로 지정
#include "/CPower/Myproject/header.h" // Linux 상에서의 절대경로 지정
그러나 절대경로를 지정해서 헤더파일을 선언하면 다른 컴퓨터에서 컴파일 하는 일이 매우 번거로워진다. 그래서 #include 문에서는 절대경로를 사용하지 않는다. 대신에 상대경로를 사용한다.
상대경로의 지정 방법
"절대로 경로가 변경되지 않는다. 컴퓨터를 옮겨도 지정한 경로는 변경되지 않는다." - 절대경로
"실행하는 컴퓨터의 환경에 따라서 경로가 바뀐다" - 상대경로
#include "Release\header0.h"
"소스파일이 있는 디렉터리의 하위 디렉터리인 Release 디렉터리에 존재하는 header0.h를 포함하라"
#include "..\CProg\header1.h"
"한 단계 상위 디렉터리의 하위 디렉터리인 CProg에 존재하는 header1.h를 포함하라."
#include "..\..\Myheader\header2.h"
"두 단계 상위 디렉터리의 하위 디렉터리인 MyHeader에 존재하는 header2.h를 포함하라."
상대경로를 기반으로 헤더파일을 선언하면, 드라이브 명이나 디렉터리 위치에 덜 영향을 받으므로 실제로는 상대경로를 기반으로 헤더파일이 선언된다.
헤더파일에 무엇을 담으면 좋겠습니까?
extern int num;
extern void Increment(void); // extern 생략 가능
이 처럼 외부에 선언된 변수에 접근하거나 외부에 정의된 함수를 호출하기 위한 선언들인데, 이들은 둘 이상의 소스파일로 이뤄진 프로그램에서 당연히 삽입될 수 밖에 없는 유형의 선언들이다. 그런데 필요할 때마다 매번 삽입하는 것은 번거로운 일이므로 이들 선언을 헤더파일에 모아두고 필요할 때마다 헤더파일을 포함시키는 방법을 선택한다. (P.592)
매크로의 명령문도 파일 단위로만 유효 하다.(선행처리기도 파일 단위로 선행처리를 한다.)
헤더파일이 존재하지 않았다면 헤더파일로 호출하고 있는 함수 등을 각각의 소스파일에 추가해야하는데(함수선언) , 헤더파일을 사용함으로써 그 번거로움을 없애준다.
즉, 헤더파일에는 다수의 소스파일에서 필요로하는 정의들을 묶어두는 용도로 헤더파일을 정의하면 좋다.
기본적으로 필요한 소스코드만들고 그 소스코드의 호출문을 헤더파일로 만듬 그 이후에 기본헤더파일을 불러와 그 파일을 기반으로 소스코드를 만들고 그 소스코드의 헤더파일을 만든다. 반복
extern 선언은 헤더파일에 넣어야 한다.
구조체의 정의는 어디에 둘까요? 그런데 중복은 안됩니다.
컴파일러는 파일 단위로 컴파일을 진행하기 때문에 외부에 있는 구조체변수를 참조하려면 구조체의 정의를 내부에서도 해줘야 한다. 그러므로 구조체의 선언 및 정의는 헤더파일에 삽입하는 것이 좋다. 그러나 하나의 소스파일 내에서만 사용이 되는 구조체라면 소스파일에 정의하는 것도 괜찮다.
실무에선 대부분 헤더파일에 정의를 한다.
헤더파일의 중복삽입 문제
헤더파일이 중복삽입이 되면 컴파일 에러가 발생한다. 그러나 프로그램이 복잡해질수록 중복삽입이 되는경우는 비일비재 하다.
파일이 떨어져있을땐 상관이 없으나,
하나의 파일안에 구조체 정의가 중복으로 들어가면 안된다. 중복정의에 의한 문제로 컴파일 에러 대상이 된다.
구조체의 정의도 그 유효함이 파일단위로 제한되기 때문이다.
헤더파일 정의시에도 헤더파일을 include 할 수 있다.
헤더파일을 중복해서 삽입하면 문제가 되나요?
헤더파일의 중복삽입 자체는 문제가 되지 않는다. 왜냐하면 이는 컴파일러에게 전달하는 메세지에 지나지 않기 때문이다. 그리고 이러한 유형의 선언은 실행파일의 크기와도 상관이 없다(컴파일러가 컴파일을 할 수 있도록 도움을 줄 뿐이므로).
하지만 구조체의 정의는 이야기가 다르다. 이는 컴파일을 하는데 도움을 주는 정보가 아닌, 실행파일의 내용에 직접적인 연관이 있는 정보이다. 구조체를 어떻게 정의하느냐에 따라서 실행파일의 크기뿐만 아니라 실행파일의 내용도 달라지지 않겠는가? 따라서 이러한 형태의 정의는 두 번 이상 중복될 수 없다.
조건부 컴파일을 활용한 중복삽입 문제의 해결
이는 '조건부 컴파일을 위한 매크로'에서 찾을 수 있다.
#ifndef __STDIV2_H__
#define __STDIV2_H__
typedef struct div
{
int quotient; // 몫
int remainder; // 나머지
} Div;
#endif
------------------------------------------
#ifndef __STDIV4_H__
#define __STDIV4_H__
#include "stdiv2.h"
Div IntDiv(int num1, int num2);
#endif
이 처럼 조건부 매크로로 중복삽입을 막을수 있다.