[C] 소스 코드를 여러 파일로 나누기 (feat. 컴파일, 링킹)
소개
작성한 소스 코드의 양이 많아지면 이를 하나의 파일로 관리하기 보다 여러 파일로 나누어 관리하는 게 좋다. 여러 파일로 분리하면 코드의 구조를 보다 명확하게 파악할 수 있고, 코드 수정 및 유지 보수에 유리하기 때문이다. 또한, 이를 통해서 필요한 파일만 컴파일 함으로써 컴파일 시간을 줄일 수 있다. 이번 글에서는 C언어에서 소스코드를 여러 파일로 나누어 관리하는 방법을 살펴보려 한다.
아래의 코드 예시를 통해 소스 코드를 여러 파일로 나누는 방법을 살펴보려한다. main.c 파일을 만들고 아래와 같이 작성해보자.
#include <stdio.h>
int add(int a, int b)
{
printf("add function\n");
return a + b;
}
int main()
{
int ret = add(1, 3);
printf("result = %d\n", ret);
}
예시는 오픈소스 컴파일러인 gcc를 사용하여 컴파일할 예정이다. gcc가 설치되어 있지 않다면 링크를 참고하여 설치하자.
커맨드 프롬프트(cmd)에 다음의 명령어을 입력하여 컴파일 해보자. -o 옵션 뒤에는 gcc 실행 결과로 나오는 파일의 이름을 지정할 수 있다.
gcc main.c -o main.exe
컴파일 결과로 main.exe 파일이 나온 걸 확인할 수 있고 main.exe 파일을 실행하면 아래와 같이 결과가 실행된다.
add function
result = 4
선언과 구현의 분리
C언어에서 파일 분리를 하려면 함수의 선언과 구현을 분리해야한다. 선언과 구현을 다른 파일로 나누어 관리하기 때문이다. 먼저 예시 코드의 add 함수의 선언과 구현을 분리하면 다음과 같다.
#include <stdio.h>
int add(int a, int b);
int main()
{
int ret = add(1, 3);
printf("result = %d\n", ret);
}
int add(int a, int b)
{
printf("add function\n");
return a + b;
}
이제 본격적으로 파일을 분리해보자. main.c 파일에 아래와 같이 add 함수의 선언부만 남겨 놓는다.
// main.c
#include <stdio.h>
int add(int a, int b);
int main()
{
int ret = add(1, 3);
printf("result = %d\n", ret);
}
main.c 파일을 컴파일 하는데 있어 add 함수의 선언 부분만 있으면 된다. 그렇기 때문에 구현부는 따로 분리한다. add 함수의 구현부는 add.c 파일을 만들어 붙여넣자.
// add.c
#include <stdio.h>
int add(int a, int b)
{
printf("add function\n");
return a + b;
}
이제 다시 main.c 파일을 컴파일 해보자.
gcc main.c -o main.exe
오류 메시지가 뜨며 컴파일이 되지 않는다. 에러 내용을 살펴보면 add 함수를 찾을 수 없다는 메시지가 뜬다.
이번엔 add.c 파일을 컴파일 해보자.
gcc add.c -o add.exe
이번에도 오류가 뜨며 컴파일이 되지 않는다. 이번에는 main 함수를 찾을 수 없다는 오류가 발생한다.
이 문제를 해결하려면 앞서 이야기했던 컴파일이라는 과정이 어떻게 이루어지는지 알아야한다. 다음 장에서는 컴파일 과정이 어떻게 이루어졌는지 살펴보고 위의 문제를 해결해보자.
컴파일 과정
컴파일은 소스코드를 컴퓨터가 이해할 수 있는 기계어로 변환하는 과정이다. 이러한 컴파일 과정은 세부적으로 구분하면 전처리-컴파일-링크 과정으로 분리하여 볼 수 있다. 전처리 단계에서는 소스 코드 내에 포함된 전처리 지시어(예: #include, #define 등)을 처리한다. 컴파일 단계에서는 전처리기에 의해 변경된 소스코드를 문법에 따라 분석하여 중간 생성물인 오브젝트 코드를 생성한다. 그리고 링크 단계에서 컴파일된 각각의 오브젝트 코드들을 하나의 실행 파일로 묶는 작업을 한다.
gcc에서 -c 옵션은 링킹 과정 전 컴파일 과정 까지만 진행시키고 그 결과물로 오브젝트 파일(.o)을 생성한다.
cmd에 아래의 명령어를 입력하여 main.c를 컴파일 과정 까지만 진행시켜 보자.
gcc -c main.c
main.o 파일이 생성된 걸 확인할 수 있다.
마찬가지로 add.c 파일도 -c 옵션으로 컴파일 해본다.
gcc add.c -c
add.o 파일이 생성된 걸 확인할 수 있다.
이제 오브젝트 파일을 아래의 명령어로 링크해본다.
gcc -o output main.o add.o
output.exe 실행파일이 생성된 것을 확인할 수 있다.
위의 과정을 한 번에 수행하려면 아래 같이 명령어를 입력해도 된다.
gcc -o output main.c add.c
그러나, 컴파일과 링킹 과정을 분리하면 다음과 같은 방법이 가능해진다. 예를 들어, add.c 파일의 일부를 수정한 경우 전체 체 파일을 컴파일 하지 않고 add.c 파일만 컴파일을 수행한 후 다른 코드의 오브젝트 파일과 링킹할 수 있다. 지금은 파일이 몇 개되지 않아서 컴파일 시간이 얼마 걸리지 않지만 소스코드 양이 많아 지면 전체 파일을 컴파일 하지 않고 수정된 파일만 컴파일함으로써 컴파일 시간을 단축시킬 수 있다.
헤더 파일
main.c 코드 상단을 보면 add 함수에 대한 정의가 되어 있다.지금은 add.c 파일에 하나의 함수밖에 없어 이를 직접 적었지만, 함수가 많아진다면 이를 일일이 적는 것은 번거로운 일이다. 이럴 때 헤더파일을 사용하면 된다.
main.c 파일을 다음과 같이 수정한다.
// main.c
#include <stdio.h>
#include "add.h"
int main()
{
int ret = add(1, 3);
printf("result = %d\n", ret);
}
add.h 파일을 아래와 같이 추가한다.
// add.h
#pragma once
int add(int a, int b);
아래의 명령어로 컴파일하면 컴파일이 잘 되는 것을 확인할 수 있다.
gcc -o output main.c add.c
컴파일러는 소스 파일이 위치한 경로에서 헤더 파일을 찾는다. 헤더 파일이 다른 곳에 위치한 경우 컴파일러가 헤더파일을 찾을 수 있도록 경로를 추가해줘야 한다. main.c 파일은 전처리 과정을 통해 add.h 파일의 내용을 복사해온다. 전처리 과정을 거치고 나면 위의 헤더파일을 분리하기 전 파일 내용과 동일하다.
라이브러리
C언어에서 잘 만들어진 함수가 담긴 파일은 여러 가지 방법으로 배포할 수 있다. 대표적으로는 소스 파일 그대로를 배포할 수 있다. 이 파일을 사용하는 사용자는 소스 코드 파일을 다운로드 받아서 이를 자신이 작성한 코드와 함께 컴파일하여 사용하면 된다. 다른 방법으로는 미리 컴파일해서 오브젝트 파일을 배포하는 방법이 있다. 이 경우 사용자는 오브젝트 파일을 받아 기존 코드에 링크를 하여 사용하면 된다. 또 다른 방법으로는 연관된 오브젝트 파일을 묶어서 한개의 라이브러리 파일로 배포하는 것이다. 사용자는 여러 오브젝트 파일을 받는 대신에 한 개의 라이브러리와 관련 헤더를 받아 사용하면 된다.
예시와 함께 살펴보자.
sub.h와 sub.c 를 작성해보자.
// sub.h
#pragma once
int sub(int a, int b);
// sub.c
#include <stdio.h>
int sub(int a, int b)
{
printf("sub function\n");
return a - b;
}
main.c 파일은 다음과 같이 수정한다.
// main.c
#include <stdio.h>
#include "add.h"
#include "sub.h"
int main()
{
int ret1 = add(1, 2);
int ret2 = sub(1, 2);
printf("result = %d, %d\n", ret1, ret2);
}
add.o 파일과 sub.o 파일을 아래 명령어를 통해 하나의 라이브러로 만든다.
ar rcs libfile.lib add.o sub.o
ar 명령어를 사용하여 라이브러리를 만들 수 있으며 rcs는 옵션을 의미한다. 각 옵션이 의미하는 바는 링크를 통해 확인할 수 있다. 이를 통해 라이브러리 파일인 libfile.lib을 생성한다.
라이브러라 파일은 다음과 같이 사용하면 된다.
gcc -I. -o output main.c libfile.lib
output.exe 파일이 잘 생성된 걸 확인할 수 있다. -I 옵션을 통해 헤더 파일의 위치를 알려준다. 헤더 파일은 현재 폴더에 있으므로 .을 입력하면 된다.