헤더파일과 소스파일에 대해 우리가 알고있는 것을 사용하여 프로그램을 여러개의 파일로 나누는 간단한 기술을 개발해보자. 함수에도 집중하고 함수와 같은 원리가 적용되는 외부 변수에도 집중해야 한다. 프로그램에 필요한 함수가 무엇인지, 논리적으로 관련된 그룹의 함수들을 어떻게 배치해야할 것인지에 대해 이미 결정이 되어있어서 프로그램이 이미 다 디자인되었다고 가정할 것이다.(프로그램 디자인에 대해선 Chapter 19에서 알아볼 것이다)
각각의 함수의 집합은 분리된 소스파일로 들어갈 것이다(이러한 파일에 foo.c
라는 이름을 사용하자). 추가로, 확장자가 .h
인 소스파일과 이름이 동일한 헤더파일을 만들 것이다(우리의 경우엔 foo.h
). 우리는 foo.c
에 정의된 함수들의 prototype을 foo.h
에 넣을 것이다.(foo.c
에서만 사용하도록 설계된 함수는 foo.h
에 선언되어선 안된다. 우리의 예시에선 여기에 해당하는 함수는 read_char
이다.) foo.c
에 정의된 함수의 호출을 필요로 하는 각각의 소스파일에 foo.h
를 포함시킬 것이다. foo.c
에 foo.h
를 포함하는 것으로 컴파일러가 foo.c
내부의 정의와 foo.h
의 prototype의 일치를 확인할 수 있도록 할 것이다.
main
함수는 프로그램의 이름과 일치하는 파일에 들어갈 것이다. 만약 우리가 bar
라고 알려진 프로그램을 원한다면, main
은 파일 bar.c
에 들어가는게 맞을 것이다. 다른 파일에서 호출되지 않는 한 main
이 있는 파일에 또다른 함수가 있을 수도 있다.
PROGRAM: Text Formatting
우리가 논의했던 기술을 표현하기 위해, justify
라는 이름의 작은 text formatting 프로그램에 이 기술을 적용시켜볼 것이다. justify
의 샘플이 되는 입력값으로, 우리는 The development of the C programming language
(in History of Programming Languages II
, edited by T. J. Bergin, Jr., and R. G. Gibson, Jr., Addison-Wesley, Reading, Mass., 1996, pages 671-687)의 문장을 포함한 파일(아주 안타까운 서식을 가졌다)인 quote
를 사용할 것이다.
C is quirky, flawed, and an
enormous success. Although accidents of history
surely helped, it evidently satisfied a need
for a system implementation language efficient
enough to displace assembly language,
yet sufficiently abstract and fluent to describe
algorithms and interactions in a wide variety
of environments.
-- Dennis M. Ritchie
UNIX나 Windows prompt에서 프로그램을 실행하기 위해서, 우리는 아래의 커맨드를 입력할 것이다.
justify <quote
<
심볼은 operating system에게 justify
는 키보드로 입력값을 받아들이는게 아닌, 파일 quote
로부터 입력값을 읽도록 알려준다. 이 특징은 UNIX, Windows 등의 operating system에서 지원되는 input redirection
이라고 불리는 특징이다. quote
파일이 입력값으로 주어졌을때, justify
프로그램은 아래의 output을 출력한다.
C is quirky, flawed, and an enormous success. Although
accidents of history surely helped, it evidently satisfied a
need for a system implementation language efficient enough
to displace assembly language, yet sufficiently abstract and
fluent to describe algorithms and interactions in a wide
variety of environments. -- Dennis M. Ritche
justify
의 output은 화면에 평범하게 나타나지만, output redirection
을 사용하여 output을 파일에 저장할 수 있다.
justify <quote >newquote
justify
의 output은 파일 newquote
에 들어간다.
일반적으로, justify
의 output은 input과 동일한데, 차이점은 추가적인 공백과 비어있는 행이 삭제되고 행들이 채워지고 조정된다는 점이다. "행을 채운다"의 의미는 하나의 단어가 더 들어간다면 행이 넘치는 상태까지 단어를 추가한다는 의미이다. "행을 조정한다"의 의미는 각각의 행이 실제로 같은 길이(60문자)가 되도록 단어들 사이에 추가적인 공백을 추가하는 것이다. 조정은 행 내부에서 단어들 사이의 공백이 동일하도록(또는 가능한 비슷하도록) 이루어져야 한다. output의 마지막 행은 조정되지 않을 것이다.
20문자를 넘는 단어는 없다고 가정할 것이다(문장 부호는 인접한 단어의 일부라고 여길 것이다).
당연하게도, 이것은 조금 제한적이긴 하지만 프로그램이 일단 작성되고 디버깅되면 여기에 대한 제한을 쉽게 늘릴 수 있고, 사실상 이 제한을 절대 초과할 수 없다. 만약 프로그램이 긴 단어를 마주쳤다면, 처음 20글자 이후의 모든 문자를 무시할 것이고 하나의 별표(*
)로 대체할 것이다.
antidisestabilishmentarianism
위의 단어는 아래의 단어로 출력될 것이다.
antidisestablishment*
이제 우리는 프로그램이 무엇을 해야하는지 이해했고, 어떻게 설계할지 생각해볼 시간이다. 단어를 읽었을 때 한 단어씩 계속 작성하는 것이 아니고, 대신에 행을 채우기에 충분할 때까지 "line buffer"에 단어들을 저장하는 프로그램을 생각해보는 것으로 시작하자. 더 깊이 생각해보면, 프로그램의 핵심은 아래와 같은 루프가 될 것이다.
for (;;) {
read word;
if (can't read word)
{
write contents of line buffer without justification;
terminate program;
}
if (word doesn't fit in line buffer)
{
write contents of line buffer with justification;
clear line buffer;
}
add word to line buffer;
}
우리는 단어를 처리하는 함수와 라인 버퍼를 처리하는 함수를 필요로 하기 때문에 프로그램을 3개의 소스파일로 나누고, 하나의 파일(word.c
)에는 단어와 관련된 모든 함수를, 또다른 파일(line.c
)에는 line buffer와 관련된 모든 함수를 넣자. 세번째 파일(justify.c
)은 main
함수를 가질 것이다. 이 파일들에 더해, 우리는 2개의 헤더파일이 필요한데 word.h
와 line.h
이다. word.h
파일은 word.c
내부의 함수들에 대한 prototype을 가진다. line.h
는 line.c
내부의 함수들에 대한 prototype을 가진다.
메인 루프를 조사해보면, 단어와 관련된 함수 중 우리가 필요한 것은 오직 read_word
함수뿐이라는 것을 알 수 있다(함수가 input 파일의 끝에 도달해서 read_word
가 단어를 읽을 수 없다면, "빈" 단어를 읽었다고 가장(위장)하는 것으로 메인 루프에 신호를 보낸다). 결과적으로, word.h
파일은 작은 파일이다.
word.h
#ifndef WORD_H
#define WORD_H
/************************************************************
* read_word: Reads the next word from the input and *
* stores it in word. Makes word empy if no *
* word could be read because of end-of-file. *
* Truncates the word if its length exceeds *
* len. *
************************************************************/
void read_word(char *word, int len);
#endif
어떻게 WORD_H
macro가 word.h
를 두 번 이상 포함하는 것을 방지하는지 주목해야한다. word.h
가 진정으로 필요하지 않더라도 이러한 방식을 통해 모든 헤더파일을 보호하는 것은 좋은 습관이다.
line.h
파일은 word.h
처럼 짧지 않다. 위에서 메인 루프의 간략한 개요를 보았었는데, 아래의 기능을 수행하는 함수가 필요하다.
우리는 이 함수들을 flush_line
, space_remaining
, write_line
, clear_line
, add_word
이라고 부를 것이다. line.h
헤더파일은 아래와 같은 모습을 보일 것이다.
line.h
#ifndef LINE_H
#define LINE_H
/************************************************************
* clear_line: Clears the current line. *
************************************************************/
void clear_line(void);
/************************************************************
* add_word: Adds word to the end of the current line. *
* If this is not the first word on the line, *
* puts one space before word. *
************************************************************/
void add_word(const char *word);
/************************************************************
* space_remaining: Returns the number of characters left *
* in the current line. *
************************************************************/
int space_remaining(void);
/************************************************************
* wrtie_line: Writes the current line with *
* justification. *
************************************************************/
void write_line(void);
/************************************************************
* flush_line: Writes the current line without *
* justification. If the line is empty, does *
* nothing. *
************************************************************/
void flush_line(void);
#endif
word.c
와 line.c
파일을 작성하기 전에, 우리는 메인 프로그램인 justify.c
를 작성하기 위해 word.h
와 line.h
에 선언된 함수를 사용할 수 있다. justify.c
를 작성하는 것은 대부분 우리가 생각한 원래의 루프 설계를 C언어로 번역하는 문제이다.
justify.c
/* Formats a file of text */
#include <string.h>
#include "line.h"
#include "word.h"
#define MAX_WORD_LEN 20
int main(void)
{
char word[MAX_WORD_LEN+2];
int word_len;
clear_line();
for (;;)
{
read_word(word, MAX_WORD_LEN+1);
word_len = strlen(word);
if (word_len == 0)
{
flush_line();
return 0;
}
if (word_len > MAX_WORD_LEN)
word[MAX_WORD_LEN] = '*';
if (word_len + 1 > space_remaining())
{
write_line();
clear_line();
}
add_word(word);
}
}
line.h
와 word.h
를 둘다 포함하는 것은 컴파일러가 justify.c
를 컴파일할 때 각각의 파일에 있는 함수 prototype에 접근할 수 있도록 해준다.
main
은 20 글자를 넘는 단어들을 조작하는 것에 대해 트릭을 사용한다. read_word
가 호출되었을 때, main
은 21 글자를 넘어가는 어떠한 단어든 잘라내도록 한다(truncate). read_word
가 반환된 후, main
은 word
가 20글자보다 긴 문자열을 가지고 있는지 확인한다. 만약 그렇다면 읽힌 단어는 반드시 최소 21 글자 이상을 가지고 있는 것이고, 그래서 main
은 21번째 문자를 별표(*
)로 대체한다.
이제 word.c
를 작성할 시간이다. 지금은 word.h
헤더파일이 read_word
하나의 함수에 대한 prototype만 가지고 있지만, 만약 필요하다면 word.c
에 다른 추가적인 함수를 넣을 수도 있다. 우리가 read_char
라는 작은 "도움을 주는" 함수를 추가한다면, read_word
가 더 작성하기 쉬워진다. read_char
가 단일 문자를 읽는 작업을 하도록 하고, 만약 개행문자나 탭(tab)을 발견한다면, 이를 공백(space)로 변경하도록 하자. read_word
가 getchar
대신에 read_char
를 호출하는 것으로 개행문자와 탭을 공백으로써 처리하는 문제를 해결할 수있다.
word.c
#include <stdio.h>
#include "word.h"
int read_char(void)
{
int ch = getchar();
if (ch == '\n' || ch == '\t')
return ' ';
return ch;
}
void read_word(char *word, int len)
{
int ch, pos = 0;
while ((ch = read_char()) == ' ')
;
while (ch != ' ' && ch != EOF)
{
if (pos < len)
word[pos++] = ch;
ch = read_char();
}
word[pos] = '\0';
}
read_word
에 대해 토론하기전에, read_char
함수 내부에서 getchar
의 사용과 관련하여 두 개의 코멘트가 있다. 일단 getchar
는 char
값 대신에 int
값을 반환한다는 것이다. 이것이 read_char
내부의 변수 ch
가 int
형 자료형으로 선언된 이유이고, read_char
의 return type이 int
인 이유이다. 또한, getchar
는 더이상 읽을 수 없을 때 EOF
값을 반환한다.(일반적으로 input 파일의 끝에 도달했을 때)
read_word
는 두개의 루프로 구성이 되어있다. 첫번째 루프는 공백을 스킵하는 것으로 처음으로 공백이 아닌 문자가 나오는 지점에서 멈춘다. (EOF
는 공백이 아니기 때문에, input 파일의 끝에 도달했다면 루프가 멈춘다) 두번째 루프는 공백(space)이나 EOF
를 마주칠때 까지 문자를 읽는다. len
한계에 도달에 도달할 때까지 word
내부에 문자들을 저장한다. read_Word
내부의 마지막 구문은 단어가 null 문자로 끝나도록 해주어 word
가 문자열임을 나타낸다. 만약 read_word
가 공백이 아닌 문자를 찾기 전에 EOF
를 마주쳤다면, pos
는 0으로 끝날 것이고 word
가 빈 문자열이 될 것이다.
남은 파일은 이제 line.c
이다. line.c
는 line.h
파일에 선언된 함수의 정의를 가져야하고, line.c
는 line buffer의 상태를 추적하는 변수를 가지고 있어야 한다. 변수 line
은 현재의 행에 문자를 저장할 것이다. 엄밀히 말해서, 우리가 필요한 변수는 오직 line
뿐이다. 그렇지만 속도와 편리함을 위해 두개의 또다른 변수인 line_len
(현재 행의 문자 수)과 num_words
(현재 행의 단어 수)를 사용할 것이다.
line.c
#include <stdio.h>
#include <string.h>
#include "line.h"
#define MAX_LINE_LEN 60
char line[MAX_LINE_LEN+1];
int line_len = 0;
int num_words = 0;
void clear_line(void)
{
line[0] = '\0';
line_len = 0;
num_words = 0;
}
void add_word(const char *word)
{
if (num_words > 0)
{
line[line_len] = ' ';
line[line_len+1] = '\0';
line_len++;
}
strcat(line, word);
line_len += strlen(word);
num_words++;
}
int space_remaining(void)
{
return MAX_LINE_LEN - line_len;
}
void write_line(void)
{
int extra_spaces, spaces_to_insert, i, j;
extra_spaces = MAX_LINE_LEN - line_len;
for (i = 0; i < line_len; i++)
{
if (line[i] != ' ')
putchar(line[i]);
else
{
spaces_to_insert = extra_spaces / (num_words - 1);
for (j = 1; j <= spaces_to_insert + 1; j++)
putchar(' ');
extra_spaces -= spaces_to_insert;
num_words--;
}
}
putchar('\n');
}
void flush_line(void)
{
if (line_len > 0)
puts(line);
}
line.c
내부의 대부분의 함수는 작성하기 쉽다. 트릭은 write_line
하나인데, 행을 정렬해서 쓴다(write). write_line
은 line
내부에 문자를 한글자씩 쓴다. 각각의 단어들의 쌍 사이에 추가적인 공백(space)이 필요하다면 공백에서 멈춘다. 추가적인 공백의 개수는 spaces_to_insert
에 저장되는데, extra_spaces / (num_words - 1)
의 값을 가지고, extra_spaces
는 최대 행의 길이와 실제 행의 길이의 차이값이다. extra_spaces
와 num_words
는 각 단어가 출력된 이후에 바뀌는데, spaces_to_insert
또한 바뀐다. 만약 extra_spaces
가 처음에 10이고, num_words
가 5라면, 첫번째 단어에는 2개의 추가적인 공백이 있을 것이고, 두번째도 2개의 공백, 3번째는 3개의 공백, 4번째는 3개의 공백이 있을 것이다.
Section 2.1에서 우리는 하나의 파일에 있는 컴파일하고 링크하는 과정을 알아보았었다. 여러 개의 파일을 가진 프로그램에 대해서 확장시켜보자. 큰 프로그램을 빌드하는 것은 작은 것을 빌드하는 것과 동일하게 기본적인 단계를 필요로 한다.
.o
의 확장자를 가지고, Windows에서 obj
의 확장자를 가진다.대부분의 컴파일러는 하나의 단계로 프로그램을 빌드할 수 있도록 해준다. 예를 들어 GCC 컴파일러로, Section 15.3의 justify
프로그램을 빌드하기 위한 커맨드는 아래와 같다.
gcc -o justify justify.c line.c word.c
세 개의 소스파일은 처음에 오브젝트 코드로 컴파일된다. 오브젝트 파일은 그 후 자동적으로 링커에게 전달되고, 하나의 파일로 결합한다. -o
옵션은 실행 파일(executable file)의 이름을 justify
로 하길 원할 때 명시한다.
모든 소스파일의 이름을 커맨드라인에 넣는 것은 빠르게 지루해질 것이다. 아주 가끔이지만, 만약 최근에 변경한 사항으로 인해 영향을 받는 것이 하나가 아니라서 모든 소스파일을 다시 컴파일 해야하고 프로그램을 다시 빌드해야할 때 많은 시간을 낭비할 수도 있다.
큰 프로그램을 빌드하는 것을 더 쉽게 하기 위해, UNIX는 makefile
이라는 개념을 만들었다. makefile은 프로그램을 빌드하기 위해 필요한 정보들을 가지고 있는 파일이다. makefile은 프로그램의 일부인 파일의 목록만 가지고 있는 것이 아니고, 파일의 의존성(dependencies) 또한 설명한다. bar.h
파일을 포함한 foo.c
파일을 생각해보자. 우리는 foo.c
가 bar.h
에 "의존한다"라고 말하기 때문에 bar.h
를 수정하는 것은 foo.c
를 다시 컴파일하는 것을 필요로 한다.
justify
프로그램에 대한 UNIX makefile을 아래에 서술해놓았다. makefile은 컴파일과 링크 과정에 GCC를 사용한다.
justify: justify.o word.o line.o
gcc -o justify justify.o word.o line.o
justify.o: justify.c word.h line.h
gcc -c justify.c
word.o: word.c word.h
gcc -c word.c
line.o: line.c line.h
gcc -c line.c
네 개의 행의 그룹이 있다. 각각의 그룹은 규칙(rule
)로 알려져있다. 첫째줄에 있는 각각의 규칙은 대상 파일(target file)이 주어지고, 그 파일이 어디에 의존하는지 나타낸다. 두번째 행은 대상(target)이 의존하는 파일의 수정때문에 다시 빌드될 필요가 있을 때 실행될 커맨드이다.
첫번째 규칙에서는, justify
(executable file)가 target이다.
justify: justify.o word.o line.o
gcc -o justify justify.o word.o line.o
첫번째 행은 justify
가 justify.o
, word.o
, line.o
에 의존한다는 것을 나타낸다. 만약 마지막으로 빌드 되었을 때로부터 이 3개의 파일 중 어떤 것이라도 수정된다면 justify
는 다시 빌드될 필요가 있다. 아래의 행에 나타낸 커맨드는 어떻게 다시 빌드(rebuilding)가 이루어지는지 보여준다.(gcc
커맨드를 사용하는 것으로 3개의 오브젝트 파일을 링크시킬 수 있음)
두번째 규칙에선 justify.o
가 target이다.
justify.o: justify.c word.h line.h
gcc -c justify.c
첫번째 행은 justify.c
, word.h
, line.h
에 변경점이 있을 때 justify.o
가 다시 빌드될 필요가 있다는 것을 나타낸다(word.h
와 line.h
를 언급한 이유는 justify.c
가 이 파일들을 포함하고 있고, 그렇기 때문에 두 개 파일의 변화에 잠재적으로 영향을 받기 때문이다). 다음 행은 justify.o
가 어떻게 업데이트되는지 나타낸다(justify.c
를 재컴파일하는 것으로). -c
옵션은 컴파일러가 링크 시도를 하지 않고 justify.c
를 오브젝트 파일로 컴파일하도록 한다.
일단 우리가 프로그램에 대한 makefile을 만들고 나면, 우리는 make
유틸리티를 프로그램 빌드하는 것에 사용할 수 있다. 프로그램 내부의 파일과 관련된 시간과 날짜를 확인하는 것으로make
는 파일이 out of date인지 결정할 수 있다. 그 후 make
는 프로그램을 재빌드할 때 필요한 커맨드를 보여줄 것이다.
만약 make
을 시도할 것이라면 알아야하는 몇 가지 세부사항이 있다.
Makefile
(또는 makefile
)이라는 이름의 파일안에 저장된다. make
유틸리티가 사용되었을 때, 이 이름이 해당하는 파일이 있는지 현제 디렉토리에서 자동적으로 확인할 것이다.make
를 불러오기 위해, 아래의 커맨드를 사용하면 된다.make target
target
은 makefile에 있는 target의 목록 중 하나이다. justify
를 우리의 makefile을 사용해 실행가능하도록 빌드하기 위해, 아래의 커맨드를 사용하면 된다.
make justify
make
를 불러올 때 어떠한 target도 존재하지 않는다면, 첫번째 규칙의 target을 빌드할 것이다. 예를 들어 아래의 커맨드는 justify
를 실행 가능하도록 빌드할 것인데, 왜냐하면 justify
는 우리의 makefile에서 첫번째 target이기 때문이다.make
make
는 책의 전체에서 여기에 대해서 작성해야할 정도로 충분히 복잡하다. 그래서 우리는 여기에 대한 복잡함을 탐구하려는 시도는 하지 않을 것이다. 진짜 makefile은 일반적으로 우리의 예제처럼 이해하기 쉽지가 않다. makefile에서의 중복성을 줄이고 수정하기 쉽게 만드는 많은 기술들이 있다. 그렇지만 이 기술들은 가독성을 대단히 떨어뜨리기도 한다.
모든 사람이 makefile을 사용하는 것은 아니다. 통합 개발 환경(IDE)에 의해 지원되는 "프로젝트 파일"을 포함하는 다른 프로그램 유지보수 도구들도 또한 유명하다.
컴파일 도중에 발견되지 않을 수도 있는 어떤 에러들은 링크 과정에서 발견될 수 있다. 특히, 만약 함수 또는 변수의 정의가 프로그램에서 찾을 수 없을 경우 링커는 이 정의에 대한 외부 참조를 해결할 수 없기 때문에 "undefined symbol"이나 "undefined reference"와 같은 오류를 발생시킬 것이다.
링커에 의해 발견되는 에러는 일반적으로 고치기 쉽다. 아래에 일반적인 오류들이 있다.
read_char
가 read_cahr
처럼 호출된다면, 링커는 read_cahr
이 없다고 보고할 것이다.foo.c
에 있는 함수를 찾지 못했다면, 파일에 대해 알지 못하는 것일 수도 있다. makefile이나 프로젝트 파일을 확인하여 foo.c
가 목록에 있는지 확인해야한다.<math.h>
헤더를 사용하는 UNIX 프로그램에서 발생하는 것이다. 단순히 프로그램에 헤더를 포함하는 것으로는 충분하지 않다. UNIX의 다양한 version은 프로그램이 링크될 때 -lm
옵션이 명시되는 게 필요한다, -lm
은 링커가 <math.h>
함수가 컴파일된 version인 시스템 파일을 검색하게 한다. 이 옵션을 사용하지 않는다면 "undefined reference"와 같은 메시지가 링크 과정에서 발생한다.프로그램의 개발 도중에, 프로그램의 모든 파일을 컴파일해야할 상황은 드물다. 대부분, 우리가 프로그램을 검사하고, 수정하고, 프로그램을 다시 빌드할 것이다. 시간을 절약하기 위해서는, 다시 빌드하는 과정은 마지막의 수정에 영향을 받는 파일들만 다시 컴파일하는 것이다.
Section 15.3의 개요에 있는 방법으로 각각의 소스파일에 헤더파일이 포함된 프로그램을 설계했다고 가정해보자. 수정 이후에 다시 컴파일해야되는 파일이 얼마나 많은지 보기 위해, 우리는 두 가지 가능성을 필요로 한다.
첫번째 가능성은 수정한 것이 단일의 소스파일에 영향을 미치는 경우이다. 이 경우에는, 이 파일이 반드시 다시 컴파일되어야 한다.(당연하게도 이후에 전체 프로그램이 다시 링크된다.) justify
프로그램을 생각해보자. 우리가 word.c
에 있는 함수인 read_char
를 압축하기로 결정했다고 가정해보자.
int read_char(void)
{
int ch = getchar();
return (ch == '\n' \\ ch == '\t') ? ' ' : ch;
}
이러한 수정은 word.h
에 영향을 미치지 않기 때문에, 우리는 오직 word.c
만 다시 컴파일하고 프로그램을 다시 링크하면 된다.
두번째 가능성은 수정한 것이 헤더파일에 영향을 미칠때이다. 이 경우에는, 헤더파일을 포함하고 있는 모든 파일을 다시 컴파일해야하는데, 왜냐하면 모든 파일들이 잠재적으로 수정한 것의 영향을 받기 때문이다.
예를 들어, justify
프로그램 내부의 read_word
함수를 생각해보자. main
함수가 읽은 단어의 길이를 결정하기 위해 read_word
를 호출한 이후에 바로 strlen
을 호출했음에 주목하자. read_word
가 이미 단어의 길이를 알고 있기 때문에(read_word
의 pos
변수가 길이를 추적함), strlen
을 사용하는 것은 바보같아보인다. read_word
가 단어의 길이를 반환하게 수정하는 것은 간단하다. 첫번째로, 우리는 word.h
내부의 read_word
의 prototype을 수정한다.
/************************************************************
* read_word: Reads the next word from the input and *
* stores it in word. Makes word empy if no *
* word could be read because of end-of-file. *
* Truncates the word if its length exceeds *
* len. Returns the number of characters *
* stored. *
************************************************************/
int read_word(char *word, int len);
당연하게도, 우리는 read_word
의 주석을 변경하는 것에 주의를 기울여야한다.
다음으로, word.c
에 있는 read_word
의 정의를 수정한다.
int read_word(char *word, int len)
{
int ch, pos = 0;
while ((ch = read_char()) == ' ')
;
while (ch != ' ' && ch != EOF)
{
if (pos < len)
word[pos++] = ch;
ch = read_char();
}
word[pos] = '\0'
return pos;
}
마지막으로, justify.c
에서 <string.h>
의 포함을 제거하고 main
함수를 수정해야 한다.
int main(void)
{
char word[MAX_WORD_LEN+2];
int word_len;
clear_line();
for (;;)
{
word_len = read_word(word, MAX_WORD_LEN+1);
if (word_len == 0)
{
flush_line();
return 0;
}
if (word_len > MAX_WORD_LEN)
word[MAX_WORD_LEN] = '*';
if (word_len + 1 > space_remaining())
{
write_line();
clear_line();
}
add_word(word);
}
}
일단 이러한 수정을 하고 나면, 우리는 word.c
와 justify.c
를 다시 컴파일하고 다시 링크하는 것으로 justify
프로그램을 다시 빌드할 것이다. word.h
를 포함하지 않는 line.c
를 다시 컴파일할 필요는 없으므로 우리가 수정한 것이 line.c
에는 영향을 미치지 않을 것이다. GCC 컴파일러을 통해 아래와 같은 커맨드를 사용하는 것으로 프로그램을 다시 빌드할 수 있다.
gcc -o justify justify.c word.c line.o
line.c
대신 line.o
를 언급한 것을 주의해야 한다.
makefile을 사용하는 것의 이점 중 하나는 다시 빌드하는 것이 자동적으로 조작(handled)된다는 것이다. 각각의 파일의 날짜를 검사하는 것으로, make
유틸리티는 마지막으로 빌드된 프로그램으로부터 어떤 것이 수정되었는지 결정할 수 있다. 그리고 간접적이든 직접적이든 이 파일들에 의존하는 다른 모든 파일과 함께 다시 컴파일한다. 우리가 word.h
, word.c
, justify.c
를 수정한 것을 나타내고 justify
프로그램을 다시 빌드한다면, make
는 아래와 같은 행동을 수행할 것이다.
1. justify.c
를 컴파일하는 것으로 justify.o
를 빌드한다.(justify.c
와 word.h
가 수정되었기 때문에)
2. word.c
를 컴파일하는 것으로 word.o
를 빌드한다.(word.c
와 word.h
가 수정되었기 때문에)
3. justify.o
, word.o
, line.o
를 링크하는 것으로 justify
를 빌드한다.(justify.o
와 word.o
가 수정되었기 때문에)
C 컴파일러는 일반적으로 프로그램이 컴파일될 때 macro의 값을 명시하는 방법들을 제공한다. 이 능력은 프로그램의 파일들을 수정하지 않고 macro의 값을 바꾸기 쉽게 만든다. 특히 프로그램이 makefile을 사용하여 자동적으로 빌드될 때 가치있다.
대부분의 컴파일러는 (GCC를 포함하여) -D
옵션을 지원하는데, 이 -D
옵션은 macro의 값을 커맨드 라인에 명시할 수 있다.
gcc -DDEBUG=1 foo.c
이 예제에서, DEBUG
macro는 프로그램 foo.c
내부에서 1의 값을 가지는 macro로 정의된다. 마치 아래의 행이 foo.c
의 시작부분에 나온 것처럼말이다.
#define DEBUG 1
만약 -D
옵션이 값을 명시하지 않고 macro의 이름을 사용한다면, 그 macro의 값은 1이 된다.
많은 컴파일러들은 또한 -U
옵션을 제공하는데, 이는 #undef
를 사용한 것처럼 macro를 "undefines"한다. 우리는 -U
를 사용하는 것으로 이미 정의된 매크로를 undefine하거나 -D
를 사용해 일찍이 커맨드 라인에 정의된 것을 undefine 할수있다.
소스파일을 포함하기 위해 #include
directive를 사용한 예시는 없는 것 같다. 만약 이렇게 한다면 무슨일 일어나는가?
이게 규칙에 어긋나는 것은 아닌데, 좋은 습관은 아니다. bar.c
와 baz.c
내부에서 필요한 함수 f
를 foo.c
에 정의했다고 생각해보자. 우리는 bar.c
와 baz.c
내부에 아래와 같은 directive를 넣을 것이다.
#include "foo.c"
각각의 파일들은 아주 잘 컴파일될 것이다. 문제는 나중에 나타나는데, 링커가 f
에 대한 2개의 오브젝트 코드의 복사본을 발견할 때이다. 당연하게도, bar.c
만 foo.c
를 포함하고, baz.c
는 foo.c
를 포함하지 않았다면 이 문제로부터 벗어날 수 있었을 것이다. 이 문제를 해결하기 위해, #include
는 소스파일이 아닌 오직 헤더파일에만 사용하는 것이 가장 좋다.
#include
directive에 대해 실제로 검색하는 규칙이 무엇인가?
무슨 컴파일러를 쓰느냐에 따라 달라진다. C표준에서는 #include
의 설명이 의도적으로 분명치 않게(vague) 되어있다. 만약 파일의 이름이 괄호(<
,>
)로 둘러싸여있다면, 전처리기는 이를 "implementation-defined place의 연속"이라고 볼 것이다. 만약 파일의 이름이 큰따옴표 표시("
)로 둘러싸여 있다면, 그 파일은 "implementation-defined manner로 검색"되고, 만약 찾지 못했다면 그 후 파일의 이름이 괄호(<
,>
)로 둘러싸여있는 것과 같은 방식으로 검색할 것이다. 명확한 답 없이 이렇게 많은 정보를 제공하는 것에 대한 이유는 간단하다. 모든 operating system이 계층적인 파일 시스템(tree 같은)을 가지지 못했기 때문이다.
설상가상으로 더 흥미로운 것은, C표준에서는 괄호(<
,>
)로 둘러싸인 이름이 파일 이름일 필요가 전혀 없다고 말하고, <>
를 사용한 #include
directive가 컴파일러에 의해 전체적으로 조작되도록 하는 가능성을 남겨두었다.
왜 각각의 소스파일이 자신만의 헤더파일을 필요로 하는지 이해하지 못하겠다. macro 정의, type 정의, 함수 prototype을 모두 포함한 큰 헤더파일은 안되는 것인가? 이 파일을 포함하는 것으로 각각의 소스파일은 모든 필요한 공유된 정보들에 대해 접근할 수 있을 것이다.
"하나의 큰 헤더파일"이라는 접근 방식은 확실히 작동하는 방식이다. 실제로 많은 프로그래머들이 사용한다. 그리고 이점도 가지고 있는데, 하나의 헤더파일만 가지고 있으므로 관리할 파일이 적다. 하지만 큰 프로그램에서는, 이 접근방식의 단점이 이 장점을 상쇄하고 넘어설 정도로 크다.
단일 헤더파일을 사용하는 것은 나중에 프로그램을 읽는 사람에게 유용한 정보를 제공하지 못한다. 여러 개의 헤더파일을 사용하면, 특정 소스파일에서 프로그램의 다른 부분을 사용하는 것을 빠르게 볼 수 있다.
이게 전부가 아니다. 각각의 소스파일이 큰 헤더파일에 의존한다면, 수정이 발생했을 때 모든 소스파일이 다시 컴파일되어야 할 것이다. 이것은 큰 프로그램에서 가장 큰 단점이다. 설상가상으로 , 그 헤더파일이 빈번하게 수정될 것인데, 헤더파일이 가진 정보가 아주 많기 때문이다.
extern int a[];
extern int *a;
공유된 배열이 전자와 같은 형태를 따라야 한다고 했었다. 그러면 배열과 포인터는 밀접한 관계성을 가졌기 때문에 후자처럼 작성하는 것이 규칙에 맞는가?
아니다. 표현식 내부에서 사용되었을 때, 배열은 포인터로 "변환(decay)"된다. (우리는 배열의 이름이 함수 호출에서 argument로써 사용되었을 때 이 행동을 주목했었다.) 그러나 변수 선언에서는, 배열과 포인터는 구분되는 자료형을 가진다.
실제로 필요하지 않은 헤더파일을 소스파일에 포함하는 것은 소스파일에 손상을 주는가?
헤더파일이 소스파일에 있는 선언이나 정의와 충돌하는 경우를 제외하고는 그렇지 않다. 충돌하지 않았을 때 일어날 수 있는 가장 안좋은 상황은 소스파일을 컴파일할 때 조금 더 시간이 걸릴 수 있다는 것이다.
파일 foo.c
내부의 함수를 호출할 필요가 있어서, 일치하는 헤더파일인 foo.h
포함하였다. 프로그램이 컴파일되었지만 링크가 되지 않는데 왜 그러는 것인가?
C언어에서 컴파일과 링크는 완벽하게 분리되어있다. 헤더파일은 링커가 아닌 컴파일러에게 정보를 제공하기 위해 존재한다. 만약 foo.c
내부의 함수를 호출하고 싶다면, foo.c
를 컴파일해야 하고, 링커가 함수를 찾기 위해 foo.c
에 대한 오브젝트 파일을 반드시 검색할 수 있도록 해야한다. 보통 이는 프로그램의 makefile이나 프로젝트 파일 내부에 foo.c
의 이름을 지정하는 것을 의미한다.
만약 프로그램이 <stdio.h>
내부의 함수를 호출한다면, 이는 <stdio.h>
내부의 모든 함수가 프로그램과 함께 링크된다는 것을 의미하는 것인가?
그렇지 않다. <stdio.h>
(또는 다른 헤더파일)를 포함하는 것은 링크에 어떠한 영향도 미치지 않는다. 어떠한 상황이든, 대부분의 링커는 오직 프로그램에 실제로 필요로 하는 함수들에 대해서만 링크할 것이다.
어디서 make
유틸리티를 얻을 수 있는가?
make
는 표준 UNIX 유틸리티이다. GNU version에서는 GNU Make로 알려져있는데 이는 대부분 Linux distributions 내부에 포함되어 있다. 또한 Free Software Foundation(www.gnu.org/software/make/)에서 직접적으로 확인할 수 있다.