Chapter 15. Writing Large Programs

지환·2022년 2월 12일
0
post-custom-banner

물론 몇몇 C 프로그램은 하나의 파일에 넣을 수 있지만, 대부분은 그렇지 않다.


15.1 Source File

by convention, source file은 .c 확장자를 갖는다.
여러 파일 중 하나의 source file은 무조건 main 함수를 포함해야한다.(starting point 지정)

이렇게 한 program을 여러 file로 나누면,
1. program의 구조를 보는데 용이하고
2. 각각 source file이 따로 compile될 수 있어서, 수정하기 좋다.(시간 절약)
3. 그리고 다른 프로그램에 필요한 부분만 가져가서 사용할 수도 있다.


15.2 Header Files

다른 파일의 정보를 가져오려면? -> #include
by convention, header file은 .h 확장자를 갖는다.

The #include Directive

  1. #include <filename>
    system headerfile이 있는 directory에서 찾는다.

  2. #include "filename"
    현재 directory에서 찾고, 없으면 system headerfile이 있는 디렉터리에서 찾는다.
    path를 같이 입력해도 된다.
    ex) #include "..\a\b.h"(window path)
    ex) #include "/a/b.h"(UNIX path)
    (여기서 double quote 있다고 해서 string literals인건 아니다, 전처리기가 string literal로 인식하지 않는다.)
    근데 path는 되도록 입력 안하는게 좋다. 왜냐하면 다른 machine이나 OS에서 컴파일 되면 까다로워지기 때문이다.(.. 넣어서 하는게 그나마 better)

  3. #include tokens

ex)

#if defined(AMD64)
  #define CPU_FILE "amd64.h"
#endif

#include CPU_FILE

Sharing Macro Definitions and Type Definitions

간단하다. header file에 macro definition, type difinition 넣고 필요한 파일은 해당 header file을 include하면 된다.

advantage
1. source file 각각에 모두 definition을 넣을 필요가 없어진다.
2. 수정할때도 header file만 수정하면 돼서 간단하다.
3. macro와 type 정의에 대해 일관성을 가질 수 있다.

Sharing Function Prototypes

다른 파일에 정의된 함수를 "호출"하려면 그 함수의 prototype이나 definition이 선행돼야한다.
그런데 definition은 다른 파일에 있으므로 prototype이 선행돼야한다.
여기서 prototype의 선행 없이 바로 호출할 경우 implicit declaration이 만들어지고 이는 잘 작동하지 않을 확률이 높다.(애초에 C99부터는 prototype의 선행 없이 바로 호출하는 것 자체가 안된다. 이 이유 때문에 막은 것)

그래서 prototype을 함수 호출 전에 넣어야 하는데,
(1)해당 함수 f가 필요한 모든 파일에 f의 prototype을 하나씩 넣는 방법(2)f의 prototype을 header file에 넣어서 그 header file을 include하는 방법이 있다.

1번 방법은 유지 보수에 문제가 있으므로, 2번을 사용한다.

해당 header file은 f의 definition이 있는 source file에도 include해서 definition과 declaration이 일치하는지 확인해야 한다.

Sharing Variable Declarations

얘도 바로 위에 함수 공유하는거랑 같은 논리다. declaration을 header file에 넣고, header file을 공유한다. (물론 변수 definition은 다른 source file에 있음)

int i; : define + declare
extern int i : declare

`exter int a[]` : 할당하는게 아니기 때문에 배열 길이 명시해줄 필요 없음.

얘도 함수 할 때 처럼 모든 declaration을 파일에 각각 넣어도 되지만,
일관성을 유지하기위해 header file에 declaration을 작성해서 header file을 include 한다. 추가로 definition이 있는 file에도 넣어서 맞는지 확인해야한다.

하지만 이렇게 변수를 공유하는건 문제가 될 수 있음.(19.2에서 자세히 설명)

요약
즉, 함수든 변수든 declaration을 header에 넣고, definition은 다른 source file에 정의해둔다.
그리고 해당 함수/변수가 필요한 file은 header를 include.(definition이 있는 곳에도 기본적으로 include해서 일치하는지 확인)
이건 include얘기고, linking은 다른 문제

Nested Includes

header file이 #include directives를 사용해도 OK

Protecting Header Files

header file을 두번이상 include하면, 그 파일은 두번이상 compile되고, 이는 오류로 이어질 수 있다.

ex) a2.h와 a3.h가 a1.h를 include하고,
b.c가 a2.h와 a3.h를 include하면 a1.h는 2번 compile된다.

보통 header file에 들어있는 macro definitions이나 function prototypes, variable declarations은 문제 없지만(여기서 말하는 variable declarations은 당연히 extern 키워드가 붙은 declaration),
type definitions이 문제가 된다.
(structure 같은 것도 그거 이용한 변수 definition이 아니라 declaration만 있으면 괜찮을 듯?)

그래서 header file을 multiple inclusion으로부터 보호해야 한다.

#ifndef BOOLEAN_H
#define BOOLEAN_H

#define TRUE 1
#define FALSE 0
typedef int Bool;    //얘 때문에 하는거

#endif

#error Directives in Header Files

#error directive는 header file이 include될 수 있는지 체크하기 위해 자주 쓰인다.

#ifndef __STDC__
#error This header requires a Standard C compiler
#endif

15.3 Dividing a Program into Files

기능을 하는 함수들끼리 모아서 하나의 source file에 만들어준다.

그 source file과 같은 이름의 header file을 만들어서 header file에는 prototype들을 작성해준다. 물론 공유할 필요가 없는, 해당 source file에서만 쓰이는 함수는 굳이 prototype을 적을 필요가 없다.

a.c 와 a.h를 만들었으면, a.c도 a.h를 include해서 definition과 declaration이 일치하는지 확인한다.

main 함수는 프로그램 이름과 같은 이름의 source file에 넣어준다. (ex. 프로그램 이름을 yoyo라고 하고싶다면 yoyo.c에 main 함수를 넣어줌)

정리하자면, 비슷한 기능을 하는 함수들끼리 묶어서 source file을 만들고 그에 상응하는 header file도 만든다. 그리고 이 header file 들을 메인source file에 include한다. 그리고 메인 source file에서 해당 기능을 수행하는 주요 코드를 작성한다.


15.4 Building a Multiple-File Program

하나의 file을 building할때랑 같은 과정이다.

  1. Compiling : 각각의 source file을 compile한다. header file은 include한 source file을 compile할때 같이 되므로 compiling 할 필요 없다. compiling 하면 .o 확장자의 object file이 생성된다.
  2. Linking : object file들을 묶어서 executable file을 만든다. linker는 compiling 이후에 남은 external reference를 처리해야한다.(external reference는 다른 파일에 정의된 함수나 변수를 사용할 때 발생한다.)

gcc에서 compiling + linking command 예시)
gcc -o justify justify.c line.c word.c

Makefiles

명령어에 파일 이름 하나하나 치면서 하는건 시간도 많이 걸리고, rebuilding할 때도 좀 골치아프다.

그래서 UNIX에서 큰 프로그램을 building하기 쉽게 makefile이라는 개념을 만들었다.
makefile에는 프로그램을 build하는데 필요한 정보가 저장돼있다.
파일 이름 + dependencies

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 file이 depends 하는 file들이다.

두번째 줄은 command인데, dependent files이 수정돼 target file이 rebuilt 돼야 한다면 command가 실행된다.

'-c' option은 object file만 만들고 linking은 안하는 것

이렇게 makefile을 만들어두면 빌딩할때 make utility를 사용할 수 있다.(사용법은 p.368)

여기선 makefile을 간단하게 설명했지만, 실제론 알아야 할 게 매우 많고 쉽지 않다.
최적화 하는 기술도 있고 그렇다...고 하네

다 makefile을 쓰는건 아니다. IDE에 project file 같은 게 더 유명하다.(makefile이랑 비슷한 기능을 하는거였나보네)

보면 알겠지만, header file을 따로 compile 하진 않는다.
.c 파일에 필요한 부분이 포함되어 같이 compile되기 때문이다.
그래서 makefile에선 헤더 직접 컴파일은 안하고, 그게 바뀌면 .c 파일이 컴파일되도록 하는 것
stackoverflow.com/questions/17416719/do-i-need-to-compile-the-header-files-in-a-c-program

Errors During Linking

compilation 할 때 발생하지 않았던 오류가 linking할 때 발생할 수 있다. 대부분은 고치기 쉽다.

  1. Misspellings
  2. Missing files
    : linker가 함수를 찾을 수 없다고하면, 1번 이유가 아니라면 makefile이나 project file을 확인해서 해당 함수가 정의된 file이 있는지 확인
  3. Missing libraries
    : library function이라도 못찾을 수도 있다. 예를들어 UNIX에서 <math.h> 헤더의 함수를 사용하려면 -lm option을 추가해서 linking을 해야된다.

Rebuilding a Program

program 만드는 과정에서 파일 수정 할때마다 전체 파일을 recomplie하진 않는다.

  1. 하나의 source file만 수정됐고, 그 file이 아무데도 영향을 주지 않는다면 걔만 recompile한다. 그리고 전체 object code를 relink한다.
  2. 수정된 source file이 다른 file에도 영향을 준다면, 해당되는 file 전체를 recompile한다. 그리고 전체 object code를 relink한다.

만약 justify 프로그램 만드는 예시에서 word.c만 수정한다면, 수정 후 이의 영향을 받는 justify.c와 word.c를 recopile한 후 relink한다.
따라서 다음과 같은 명령어를 사용할 수 있다.
gcc -o justify justify.c word.c line.o line.c는 recomlie 할 필요가 없으므로 line.o를 입력했다.

make utility를 사용하면, 각각 파일의 날짜를 비교해 수정됐는지 확인한다. rebuilding하기 용이하다.(자동으로 rebuilding 되는 모양이네)

Defining Macros Outside a Program

대부분 compiler는 -D option을 제공해 macro를 정의할 수 있게 한다.
gcc -DDEBUG=1 foo.c

-U option을 사용해 undefine 할 수도 있다.


Q&A

#include로 source file을 include하면 문제가 되나?
금지는 아닌데, 그렇다고 좋은 생각도 아니다.
foo.c 에 있는 함수 f를 사용하기 위해, bar.cbaz.cfoo.c를 include한다고 가정하자. compile까지는 문제 없겠지만, linker가 f의 정의가 두개나 있다는걸 보면 문제가 된다.

#include directive가 searching하는 정확한 규칙은?
compiler에 따라 달려있다. C standard도 모호하게 기술했다. brackets에 있으면 "sequence of implementation-defined places"에서 찾고, quotation marks에 있으면 "implementation-defined manner"대로 찾아보고 위 방법대로 찾는다고 한다.
모든 OS가 hierarchical (tree-like) file systems을 가지고 있지 않기때문에 이처럼 기술한 것이다.

하나의 큰 header file을 만들면 안되나?
간단한 프로그램에선 뭐 그럴수도 있는데, 큰 프로그램에선 아니다.
어떤 기능을 하는건지 알아보기도 힘들고, 무엇보다 수정할 때마다 별 상관없을 파일들도 다 recompile해야되는 경우가 생길 수 있다.

extern으로 변수 declare 할 때도 배열이랑 포인터랑 구분 안해도 괜찮나?
아니다. 구분 안되는 경우도 있는건 맞지만, variable declaration에선 분명히 다른 type이므로 구분해줘야한다.

foo.c에 있는 함수를 사용하기 위해 해당 헤더파일인 foo.h를 include했다. compiling은 되지만, linking이 안된다. 왜?
C에서 compiling과 linking은 완전히 분리된 과정이다. 헤더파일을 include하는 것은 compiler에게 정보를 주는거지 linker와는 아무 상관이 없다. linker가 foo.c의 object file을 찾도록 확인해야한다. 대부분 이 과정은 makefile이나 project file에 추가하는 것을 의미한다.

위 질문 답변에서 헤더파일 include하는게 linking이 되는게 아니면 linker는 어떤 기준으로 어떤 파일을 linking하는거지? makefile은 그냥 rebuilding하기 편하려고 쓰는거 아니었나, 거기에 왜 추가하란거지?
gcc command 입력할 때 compling하고 linking할 파일들 이름을 쭉 쓴다. 그럼 그게 linked돼서 하나의 executable file이 되는거다. makefile에 넣어두면 (command에도 당연히 들어감) 해당 command가 실행되면서 당연히 같이 liking이 되는 것

<stdio.h> include하면 그 안의 모든 함수가 linked되는건가?
아니다. #include는 linking과 아무 관련없다. 대부분 linker는 다 liking하는게 아니라, 내 프로그램에서 사용하는 함수만 linking한다.

post-custom-banner

0개의 댓글