우리가 지금까지 만든 소스 코드는 모두 하나의 소스 파일에서 만들었다. 사실 이는 큰 문제가 아니였다. 왜냐하면우리가 만든 소스 코드가 그리 길지도 않고 혼자 사용하기 때문이다. 그러나 실제로 회사에서 일을 하면 소스 코드가 수천에서 수만줄까지 가고 여려명이서 협업하여 일하기 때문에 파일을 여러개로 나눌 필요가 있다. 나눌 때에는 비슷한 작업을 하는 것 끼리 나누는 것이 좋을 것이다.
#include <stdio.h>
char compare(char *str1, char *str2);
int main() {
char str1[20];
char str2[20];
scanf("%s", str1);
scanf("%s", str2);
if (compare(str1, str2)) {
printf("%s 와 %s 는 같은 문장 입니다. \n", str1, str2);
} else {
printf("%s 와 %s 는 다른 문장 입니다. \n", str1, str2);
}
return 0;
}
char compare(char *str1, char *str2) {
while (*str1) {
if (*str1 != *str2) {
return 0;
}
str1++;
str2++;
}
if (*str2 == '\0') return 1;
return 0;
}
실행 결과
hello
hi
hello 와 hi 는 다른 문장 입니다.
위 예제는 우리가 이전에 배웠던 내용으로 만든 아주 간단한 예제이므로 이해하는데 전혀 무리가 없을 것이다. 혹시 모르니까 한 부분 만 보면
if (compare(str1, str2))
if
의 조건문으로 함수가 들어가 있는데 함수가 참이면 return 1
을 하고 거짓이면 return 0
를 하도록 함수의 정의부분에서 설정 되어 있다. if
조건문은 0만 아니면 실행되기 때문에 함수가 참이면 1 을 리턴하여 조건에 맞아 실행이 되고 0이 오면 조건문에 맞지 않아 if
문을 실행하지 않는다.
그렇다면 이제 파일을 나눠보자. compare
함수는 상당히 다른 일을 하고 있다. 따라서 굳이 main
함수와 같이 둘 필요가 없다. 따라서 다른 파일로 만들겠다.
우선 str.c
라는 파일을 만들고 기존에 작업하던 test.c
파일에서 compare
함수를 정의하는 부분만 빼서 붙어 넣었다. 따라서 현재 다음과 같다.
#include <stdio.h>
char compare(char *str1, char *str2);
int main() {
char str1[20];
char str2[20];
scanf("%s", str1);
scanf("%s", str2);
if (compare(str1, str2)) {
printf("%s 와 %s 는 같은 문장 입니다. \n", str1, str2);
} else {
printf("%s 와 %s 는 다른 문장 입니다. \n", str1, str2);
}
return 0;
}
char compare(char *str1, char *str2) {
while (*str1) {
if (*str1 != *str2) {
return 0;
}
str1++;
str2++;
}
if (*str2 == '\0') return 1;
return 0;
}
그리고 test.c
파일을 실행하면
실행 결과
hello
hi
hello 와 hi 는 다른 문장 입니다.
아까와 같이 프로그램이 잘 작동된다.
일단 프로그램이 어떻게 작동하는지 살펴보자.
정말 예전에 배워서 까먹었을 수 있는데 우리가 실행 파일을 만들기 위해서 컴퓨터는 우선 C 언어 코드를 컴퓨터가 이해할 수 있는 언어로 바꿔주는 컴파일(compile)을 진행한다. 이는 단일 소스 코드 전체를 어셈블리어 (기계어와 1 : 1 대응되어 있음) 로 변환해준다.(이 때 목적 코드라 불리는 확장자가 .o
인 파일이 생성된다). 이 과정이 끝나게 되면 링킹(linking) 이라는 과정이 진행되는데 말 그대로 각기 다른 파일에 위치한 소스 코드를 한데 엮어서 하나의 실행 파일로 만드는 과정이라 생각하면 된다.
링킹과정에서 특정한 소스 파일에 있는 함수들이 어디어디에 있는지 찾는 과정을 거치게 되는데 예를 들어서 test.c
의 경우 compare
함수가 어디있는지 찾게 된다.(눈치 빠른 사람은 printf
함수 역시 찾아야 함을 알 수 있다 이는 나중에 다루겠다.)
우리의 예제에 경우 compare
함수는 str.c
에 있기 때문에 링커(링킹을 해주는 프로그램) 는 ' test.c
에서 compare
함수를 호출하는 경우 str.c
에서 찾아라' 정도로 처리해 주게 된다. 덕분에 우리가 test.c
에서 compare
함수를 호출하더라도 str.c
의 compare
함수를 이용할 수 있게 된다.
만일 test.c
에서
char compare(char *str1, char *str2);
이 부분을 지워보면 어떻게 될까? 이렇게 된다면 컴파일러는 'main 함수에서 compare
함수를 호출 했는데 도대체 compare
함수는 어떻게 생긴 모양이야?'가 되어 컴파일시 오류가 발생한다. 물론 링커에게도 이 compare
함수가 도대체 어떻게 생긴 건지 알 수 없기 때문에 오류가 발생하게 된다.
따라서 이렇게 언제나 함수의 선언을 명시해 주는 것이 매우 중요하다.
그런데 만약에 str.c
파일에 copy
라는 함수를 추가 했다고 생각해보자. 그러면 우리는 이 함수를 test.c
에서 사용하기 위해서 copy
함수의 원형을 test.c
에 추가해야 한다. 이는 정말 귀찮은 일이 아닐 수 없다. 그리고 또한 다른 파일에서 만약 compare
함수와 copy
함수를 사용해야 한다면 거기에서 또 함수의 원형을 써줘야 한다..
이렇게 귀찮은 작업을 막기 위해서 C 언어에서 아주 놀라운 해결책을 제시했는데 바로 헤더파일 (header file)을 이용하는 것이다.
먼저 str.c
파일을 만들 때와 같이 헤더 파일을 만들어보자. 파일을 만들 때 파일의 종류를 헤더로 선택하면 된다. 필자가 사용하는 Xcode는 str.c
를 만들 때 자동으로 str.h
라는 헤더 파일이 만들어졌다. 만약 필자와 같이 자동으로 생성되지 않으면 헤더 파일을 만들고 이름을 str
로 만들면 된다. 그러면 str.h
라는 헤더 파일이 만들어질 것이다.
char compare(char *str1, char *str2);
그리고 헤더 파일에 다음과 같이 함수의 원형을 추가하자. 그 후의 각 소스 코드를 다음과 같이 수정해준다.
#include <stdio.h>
#include "str.h"
int main() {
char str1[20];
char str2[20];
scanf("%s", str1);
scanf("%s", str2);
if (compare(str1, str2)) {
printf("%s 와 %s 는 같은 문장 입니다. \n", str1, str2);
} else {
printf("%s 와 %s 는 다른 문장 입니다. \n", str1, str2);
}
return 0;
}
#include "str.h"
char compare(char *str1, char *str2) {
while (*str1) {
if (*str1 != *str2) {
return 0;
}
str1++;
str2++;
}
if (*str2 == '\0') return 1;
return 0;
}
그리고 컴파일 해보면
실행 결과
hi
hi
hi 와 hi 는 같은 문장 입니다.
다음과 같이 정상 작동 되는 것을 볼 수 있다.
먼저 test.c
를 살펴보자.
#include <stdio.h>
#include "str.h"
#include
와 같은 명령들은 전처리기(Preprocessor) 명령이라고 부르는데 이러한 명령들의 특징은 컴파일 이전에 먼저 실행된다는 것이다. 이 명령들은 우리가 지칭하는 파일의 내용을 정확히 100%
복사해서 붙여 넣는다는 것이다. 따라서 #include "str.h"
라는 명령은 str.h
에 있던 내용(여기선 char compare(char *str1, char *str2);
)으로 컴파일 이전에 100% 바뀐다.
그렇다면 #include <stdio.h>
는 어떨까? 역시 똑같다. stdio.h
에 있던 내용들이 정확히 복사 되어 컴파일 이전에 코드에 붙어버린다. 그런데 이상한 점은 stdio.h
는 <>
로 감쌌는데 str.h
는 ""
로 감쌌다는 것이다. 그 이유는 간단하다. < >
로 감싸는 헤더 파일은 컴파일러에서 기본적으로 지원하는 헤더 파일이고 " "
로 감싸는 파일은 사용자가 직접 만든 헤더 파일이다.
혹시 stdio.h
에 무엇이 써있는지 궁금하지 않나? 그 내용을 간단히 보여주면 아래와 같다.
#ifndef _STDIO_H
#define _STDIO_H 1
#define __GLIBC_INTERNAL_STARTING_HEADER_IMPLEMENTATION
#include <bits/libc-header-start.h>
__BEGIN_DECLS
#define __need_size_t
#define __need_NULL
#include <stddef.h>
#define __need___va_list
#include <bits/types.h>
#include <bits/types/FILE.h>
#include <bits/types/__FILE.h>
#include <bits/types/__fpos64_t.h>
#include <bits/types/__fpos_t.h>
#include <bits/types/struct_FILE.h>
#include <stdarg.h>
/* ... 너무 길어서 생략 ... */
더 보려면 링크 를 보자
만일 헤더 파일이라는 것이 존재하지 않았다면 우리는 printf
함수를 사용하기 위해서 위 모든 내용은 아니지만 printf
에 대한 함수 원형을 써줘야 하는데 이는
_Check_return_opt_ _CRTIMP int __cdecl printf(
_In_z_ _Printf_format_string_ const char* _Format, ...);
로 무지 복잡하게 생겼다. 아무튼, 이렇게 printf
나 sanf
함수와 같이 매번 쓰는 함수를 사용하기 위해서 위 모든 내용을 쓰는 것 대신 헤더파일 include
하나로 간단하게 해결할 수 있다.
보통 헤더파일을 만들 때 에는 그 헤더 파일의 함수가 정의 되어 있는 소스 파일의 이름을 따서 짓는게 보통이다. 위 의 경우 str.h
에 선언된 함수들이 str.c
에 정의되어 있으므로 헤더 파일의 이름을 str.h
로 하였다. 또한 한 가지 흥미로운 점은 str.c
에도 str.h
를 include
하고 있다는 것이다.
이는 다음과 같은 상황을 방지할 수 있다.
/* something.c */
int A() {
B();
return 0;
}
int B() { return 1; }
만일 이와 같은 something.c
라는 파일 있다고 생각해보자 위는 컴파일시에 오류가 발생한다. 왜냐하면 A
함수가 실행 되었을 때는 B
함수가 정의되어 있지 않기 때문이다. 따라서 B
함수의 원형을 위에 적어줘야 한다.
/* something.c */
int B();
int A() {
B();
return 0;
}
int B() { return 1; }
이와 같이 말이다. 그런데 우리는 헤더 파일에 대해서 배웠기 때문에 이렇게 할 바에는 그냥 헤더 파일에 추가해주는 게 훨 나을 것이다.
/* something.c */
#include "something.h"
int A() {
B();
return 0;
}
int B() { return 1; }
/* something.h*/
int A();
int B();
이와 같은 이유로 str.c
에서도 (이 경우에는 필요 없지만) str.h
를 include
해주는 것이다.
이제 각자 예전에 만들었던 도서 관리 프로그램의 모듈화를 해보자