여기서는 C언어 또는 C++로 작성된 소스 파일, 헤더 파일의 개념과 이들을 빌드하여 하나의 프로그램을 만드는 과정에 대해 간략하게 짚어보겠습니다. 여러 개의 소스, 헤더 파일들로 이루어진 코드를 빌드하는 과정에서, 오브젝트 파일들과 외부 라이브러리가 어떻게 개입하는지도 알아봅시다.
C언어나 C++ 소스 코드를 작성하기 위한 기본적인 문법에 대해 다룬 글들이 하단에 링크되어 있습니다. 맨 처음 입문하시는 분들에게 큰 도움이 되리라 생각합니다.
소스 파일과 헤더 파일
먼저 C/C++ 소스 파일 (source file)과 헤더 파일 (header file)의 개념과 역할에 대해 짚어볼텐데요. 이들은 C언어나 C++ 문법에 따라 명령어들을 나열한 텍스트 파일입니다. 이 명령어들을 순차적으로 실행하라고 컴퓨터에 지시를 하는 방식이죠. 변수를 선언하고 값을 정의하거나, 함수를 호출하는 것 등이 모두 명령어입니다. 그리고 이들은 C언어나 C++ 에서는 세미콜론 (;)으로 분리되어 있습니다.
헤더 파일은 소스 파일을 컴파일하기 전에 미리 처리하는 내용들을 담은 파일입니다. 확장자는 .h 이며 일반적으로 소스 파일의 맨 위에 포함되는데요. 그렇게 하면 헤더 파일의 전체 내용이 그대로 소스 파일에 대입되고, 컴파일러가 이를 가장 먼저 처리하게 됩니다. 헤더 파일에 주로 들어가는 내용들은 다음과 같은 것들이 있습니다.
- 전역 변수
서로 다른 소스 파일 사이에서 전역 변수를 공유하기 위해서는 extern 키워드를 통해서 변수의 존재를 알려줄 필요가 있습니다. 이러한 전역변수를 헤더 파일에 선언해 두면, 이를 포함한 모든 소스파일에서 해당 변수가 있다는 사실을 알고 값을 공유하게 되죠. - 함수의 프로토타입
함수가 구체적으로 어떤 역할을 하는지는 소스 파일에 정의해두고, 프로토타입은 헤더 파일에 선언하는 것이 편리합니다. 그렇게 함으로써 소스 파일에서 간단하게 프로토타입을 선언할 수 있고, 다른 소스 파일에서 정의된 함수에 대해서도 그 존재를 알게 됩니다. - 식별자 및 매크로
#define 전처리기를 통해 정의하는 식별자 또는 매크로 역시 헤더 파일에 넣어두면, 프로그램의 일관성과 통일성을 위해 도움이 됩니다. - (C++ 의 경우) 클래스 선언
멤버 변수 및 멤버함수들의 프로토타입을 가지고 클래스를 선언하는 것은 보통 헤더 파일에서 이루어집니다.
헤더 파일을 포함시키기 위해서는 #include 전처리문이 사용되는데, #include <[헤더 파일 이름]> 또는 #include "[헤더 파일 이름]" 을 소스 파일에 넣어주면 되겠습니다. 꺾은괄호 (< >)의 경우, 환경변수 등에 미리 지정된 경로에 있는 헤더 파일을 탐색하는 반면, 큰따옴표 (" ")의 경우에는 현재 디렉토리 역시도 탐색을 합니다
예를 들어서 helloworld.h 라는 헤더 파일이 있다면 다음과 같이 소스 파일에 포함시킬 수 있습니다.
#include"helloworld.h"
소스 파일에서는 필요한 헤더 파일들을 맨 위에 포함시킨 다음, 함수들의 역할을 정의합니다.
컴파일 : 소스 파일 to 오브젝트 파일
소스 파일과 헤더 파일들이 갖추어졌다면, 프로그램을 만들기 위한 첫 번째 단계는 소스파일을 가지고 CPU가 이해할 수 있는 2진법 기계어 (machine code)로 바꾸는 것입니다. 이를 컴파일이라고 하며, 그 결과로 생성되는 오브젝트 파일 (object file)에 기계어로된 정보가 들어가게 되겠습니다.
이렇게 소스 파일을 받아서 오브젝트 파일을 만들어주는 프로그램을 컴파일러라고 하며, 밑에서 설명할 링크 기능을 함께 달고 나오는 경우가 많습니다. 대표적으로 GNU 컴파일러 (gcc, g++), clang 컴파일러 (clang, clang++), 인텔 컴파일러 (icc, icpc) 등이 있으며, 마이크로소프트의 통합 개발환경인 비주얼 스튜디오 역시 C/C++ 컴파일러를 가지고 있습니다.
한 가지 짚고 넘어갈 점이라면, 여러 개 소스 파일에서 정의된 함수들을 호출하는 프로그램의 컴파일 단계에서 필요한 것은 함수의 프로토타입이라는 것입니다. 소스 파일로부터 오브젝트 파일을 만드는 단계에서는 일단 프로토타입으로 선언된 함수가 어딘가에 정의되어 있다고 가정하고 일을 진행합니다. 물론 함수의 실제 내용에 대한 정의가 소스 파일들 중 어딘가에는 있어야겠죠. 함수의 이름, 인자와 리턴값의 자료형 역시 같아야 할 것이고요. 그렇지 않을 경우, 컴파일은 무사히 넘기겠지만 링크 단계에서 오류가 날 것입니다.
만일 소스 또는 헤더 파일에 문법 오류가 있을 경우, 컴파일러가 문제가 되는 명령어의 위치를 알려주고 컴파일을 진행하지 않습니다. 이 때는 당연히 오류를 정정하고 다시 컴파일을 해야겠죠.
링크 : 오브젝트 파일 + 라이브러리 to 실행 파일
링크 단계에서는 서로 다른 오브젝트 파일들에 담긴 정보를 연결해서 실행이 가능한 프로그램을 만들게 됩니다. 각 소스 파일들을 컴파일하기 위해서는 함수들의 존재를 알리기 위한 프로토타입이 필요한 반면, 실제 구동 가능한 프로그램을 완성하기 위해서는 함수들이 실제로 무엇을 하는지를 알아야 합니다. 이를 위해서 오브젝트 파일들을 전부 모아다가 함수들의 정보를 취합하게 되죠. extern 키워드를 통해 공유되는 전역 변수 역시 링크 단계에서 연결이 됩니다.
오브젝트 파일 뿐만 아니라 외부 라이브러리 역시 이 과정에서 개입을 하게 되는데요. 라이브러리는 간단히 말하자면 자주 사용하는 함수들의 정의를 별도의 파일에 저장한 것입니다. 우리가 C언어나 C++ 소스 코드에서 입출력 함수나 수학 함수 등을 호출할 수 있는 이유는 이들이 표준 입출력 라이브러리와 수학 함수 라이브러리에 포함되어 있고, 오브젝트 파일과 함께 링크 되기 때문이죠. 컴파일러가 자체적으로 달고 나오는 표준 라이브러리가 아니라도, 라이브러리 파일의 경로를 알려주면 링크를 하는데 지장이 없습니다.
예시 : Hello World!
여기서는 "Hello World!" 프로그램을 재구성한 예시를 하나 소개합니다. 여러 개의 소스 및 헤더 파일들로 이루어진 코드를 작성하고 이를 빌드해 보도록 합시다.
main.c
// 헤더파일 포함
#include"helloworld.h"
int main(int argc, char *argv[]) {
fprintf(stdout, "\n");
// 전역 변수들의 값 정의
num_greeting = 1;
num_good_bye = 2;
// greetins 함수 호출
greeting();
// 전역 변수들의 값 정의
num_greeting = 3;
num_good_bye = 4;
// good_bye 함수 호출
good_bye();
return 0;
}
main 함수가 정의되어 있는 소스 파일입니다. 코드를 빌드하여 프로그램을 만들고 이를 실행하면, 바로 main 함수를 실행하게 되죠. 가장 먼저 전역 변수인 num_greeting, num_good_bye 및 greeting, good_bye 함수들이 있다는 걸 알려줘야 하는데요. 그 함수들이 구체적으로 어떤 역할을 하는지는 컴파일 단계에서 알 필요가 없지만, 존재한다는 것은 알아야 하기 때문에 프로토타입이 선언된 헤더 파일을 포함시켜야 하는 것입니다.
helloworld.h
/* 식별자 _HELLOWORLD_H_가
* 정의되어 있지 않을경우에만
* 헤더 파일을 포함하고,
* _HELLOWORLD_H_를 정의
* 헤더 파일이 중복으로
* 포함되는 것을 방지 */
#ifndef _HELLOWORLD_H_
#define _HELLOWORLD_H_
/* 표준 입출력 라이브러리의
* 헤더 파일 */
#include<stdio.h>
// 전역 변수
extern int num_greeting;
extern int num_good_bye;
// 함수들의 프로토타입
void greeting();
void good_bye();
#endif
각 소스 파일들에 포함된 헤더 파일입니다. 일단 전처리문을 활용하여 같은 헤더 파일이 중복으로 들어가는 것을 막을 필요가 있는데요. _HELLOWORLD_H_라는 식별자가 이러한 기능을 하고 있습니다. 한번이라도 헤더 파일이 포함되면 _HELLOWORLD_H_ 식별자가 정의되기 때문에, 그 이후로는 #ifndef (정의되지 않은 경우에만 참이 되는)조건에 의해서 헤더 파일이 중복으로 들어가지 않습니다.
그 다음으로 메시지를 출력하기 위해 필요한 표준 입출력 (standard I/O) 라이브러리의 헤더 파일인 stdio.h를 포함시켰습니다. 이렇게 하면 helloworld.h 헤더 파일을 포함시켰을 때, 표준 입출력 함수들의 프로토타입들이 같이 들어가게 되겠죠.
그 밑에는 전역 변수들인 num_greeting과 num_good_bye가 등장하는데요. extern 키워드 덕분에 헤더 파일을 포함한 모든 소스 파일에서 그 이름과 값을 공유하게 됩니다. 주의할 점은 이 변수들이 단 하나의 소스 파일에 단 한번만 선언되어야 한다는 것입니다. 중복으로 선언이 되면 링크 단계에서 오류가 나겠죠. 마지막으로 함수들의 프로토타입이 선언되어 있습니다. 그러면 헤더 파일을 포함한 소스 파일들이 greeting 및 good_bye 함수들의 존재를 알게 되는 것입니다.
greeting.c
// 헤더 파일 포함
#include"helloworld.h"
// 전역 변수
int num_greeting;
// greeting 함수 정의
void greeting() {
fprintf(stdout,
"HelloWorld!\n");
fprintf(stdout,
" num_greeting = %d\n",
num_greeting);
fprintf(stdout,
" num_good_bye = %d\n",
num_good_bye);
fprintf(stdout, "\n");
}
greeting 함수가 정의된 소스파일입니다. 여기에는 정수형 전역 변수 num_greeting이 선언되어 있고, 헤더 파일을 통해서 다른 소스 파일들이 이 변수의 존재를 알게 되죠. 한가지 더 눈여겨 볼 것은 greeting 함수 내에서 변수 num_good_bye의 값이 사용된다는 것인데요. 맨 위에 포함된 헤더 파일로부터 그 변수가 어딘가에 선언되어 있다는 걸 알았기 때문입니다.
good_bye.c
// 헤더 파일 포함
#include"helloworld.h"
// 전역 변수
int num_good_bye;
// good_bye 함수 정의
void good_bye() {
fprintf(stdout,
"Bye Bye~\n");
fprintf(stdout,
" num_greeting = %d\n",
num_greeting);
fprintf(stdout,
" num_good_bye = %d\n",
num_good_bye);
fprintf(stdout, "\n");
}
마지막으로 good_bye 함수가 정의된 소스 파일입니다. 그리고 여기에는 전역 변수 num_good_bye가 선언되어 있습니다.
GNU 컴파일러 (및 링커)를 사용하는 경우라면, 다음과 같이 프로그램을 빌드할 수 있습니다.
- 컴파일
gcc main.c -c
gcc greeting.c -c
gcc good_bye.c -c - 링크
gcc main.o greeting.o good_bye.o -o [실행파일 이름]
컴파일 단계에서 사용된 -c 옵션은 링크는 하지말고 오브젝트 파일만 생성하라는 뜻입니다. 이렇게 하면 소스 파일과 이름은 동일하지만 .o 확장자를 가진 오브젝트 파일을 얻을 수 있죠. 두 번째 링크 단계에서는 모든 오브젝트 파일들을 나열해 줍니다. 추가로 -o 옵션을 통해 실행파일의 이름을 지정해 줄 수 있습니다.
비주얼 스튜디오에서는 솔루션 탐색기를 통해 헤더 파일과 소스 파일들을 프로젝트에 추가한 다음, 빌드 버튼만 누르면 컴파일부터 링크까지 한큐에 이루어집니다. 비주얼 스튜디오에서 프로그램을 빌드하는 과정에 대해서는 다음 포스팅에 더 자세하게 소개되어 있습니다.
이렇게 해서 프로그램을 실행해 보면 다음과 같은 결과를 얻을 수 있습니다.
main 함수 내에서 greeting, good_bye 함수들이 호출되면서 "HelloWorld!" 와 "Bye Bye~" 메시지를 각각 출력합니다. 뿐만 아니라 main 함수에서 정의된 전역 변수의 값들이 greeting, good_bye 함수들에서 그대로 출력되는 것도 확인이 가능한데요. 이는 헤더 파일에 있는 extern 키워드를 통해 서로 연결이 되어 있기 때문입니다.
이상으로 간단한 예제 프로그램을 통해 소스 코드가 프로그램이 되는 여정을 살펴보았습니다. 오브젝트 파일들을 이용해서 라이브러리를 직접 만들면, 이를 두고두고 우려먹는 것도 가능한데요. 라이브러리를 만들고 C언어 및 C++ 프로그램에서 사용하는 방법에 대한 자세한 사항은 다음 포스팅에 소개되어 있습니다.
이번 포스팅에서는 C언어 및 C++ 프로그램에 대해 이야기했습니다만, 포트란 소스 파일을 가지고도 포트란 컴파일러를 통해 오브젝트 파일을 얻을 수 있습니다. 그렇게 하면 포트란에서 작성된 함수를 C/C++ 프로그램에서 호출하거나 그 반대의 경우도 가능한데요. 이 방법에 대한 것이 궁금하시다면, 다음 포스팅을 읽어보시면 큰 도움이 되리라 생각합니다.
C/C++ 프로그래밍의 기초
변수와 자료형
연산문과 사칙연산
함수와 라이브러리
조건문과 관계 연산자
반복문 (for, while)
포인터