[C언어] 파일의 분할과 헤더파일의 디자인

김민정·2024년 9월 24일
0
post-thumbnail

Chapter 27. 파일의 분할과 헤더파일의 디자인

27-1 "파일의 분할"

파일을 나눠서 각각의 파일에 용도 및 특성 별로 함수와 변수를 나눠서 저장하면 소스코드의 관리가 용이해진다.
다음 예제를 대상으로 파일을 나눠보도록 해보자.

#include <stdio.h>
int num = 0;

void Increment(void)
{
    num++;
}

int GetNum(void)
{
    return num;
}

int main()
{
    printf("num: %d \n", GetNum());
    Increment();
    printf("num: %d \n", GetNum());
    Increment();
    printf("num: %d \n", GetNum());
    return 0;
}

> 출력
num: 0 
num: 1
num: 2

이 파일을 총 세 개의 파일로 나눠서 저장한다고 가정해 보자.

위 그림과 같이 단순하게 파일 세 개로 나누면 잘 작동할까?
안타깝게도 컴파일러는 다른 파일의 정보를 참조하여 알아서 컴파일을 진행하지 않는다.
그래서 func.c를 컴파일하면 변수 num이 선언되지 않았기 때문에 컴파일 에러가 발생한다.
main.c를 컴파일해도 파일 내에서 Increment 함수가 정의된 적이 없기 때문에 에러가 발생한다.
따라서, 각 변수 또는 함수가 외부에 선언 및 정의되었다고 컴파일러에게 알려줘야한다.

여기서 사용되는 것이 extern 키워드다.
extern키워드는 변수나 함수가 외부에 선언되었음을 컴파일러에게 알릴 때 사용된다.
함수가 외부에 정의되어 있음을 알릴 때에는 extern 선언을 생략할 수 있다.

extern int num;		// int형 변수 num이 외부에 선언되어 있다.
extern void Increment(void);	// void Increment(void) 함수가 외부에 정의되어 있다.
void Increment(void);	// extern 선언 생략 가능.

따라서, 아래와 같이 정정되면 컴파일이 가능하며 extern 선언을 통해 함수 또는 변수가 외부에 선언 및 정의되어 있다는 것을 알리기만 하면되고 구체적으로 어느 파일에 선언 및 정의되어있는지 까지는 알리지 않아도 된다.

static 전역변수의 활용

이전에 static 지역변수에 대해 배웠다.
static 전역변수는 외부 파일에서의 접근을 허용하지 않을 때 사용한다. 즉, 변수의 접근범위를 파일 내부로 제한하는 것이다.


27-2 "둘 이상의 파일을 컴파일하는 방법과 static에 대한 고찰"

둘 이상의 파일을 컴파일하는 방법은 Visual Studio를 사용했을 때 사용하면 유용한 점에 대해 알아볼 것이다.

<첫 번째 방법>
이미 만들어진 파일을 프로젝트에 추가하는 방법.
'소스파일 → 추가 → 기존 항목'을 선택하여 위에서 만든 num.c, func.c, main.c 파일들을 추가한다.

<두 번째 방법>
이 방법은 소스 파일이 아닌 내가 새롭게 작성해서 사용할 때 사용하는 방법으로
'소스파일 → 추가 → 새 항목'을 선택하여 새로운 파일을 작성하면 된다.

함수의 static 선언

전역 변수와 마찬가지로 함수에서도 static 선언을 할 수 있다.
변수와 마찬가지로 파일 내에서만 접근 가능하도록 함수를 제한하는 것이다.
이는 코드에 안정성을 부여할 수 있다.


27-3 "헤더파일의 디자인과 활용"

#include 지시자의 의미

이번 예제는 #include지시자의 의미를 이해하기 위함이다.
아래 예제는 동일한 디렉터리에 존재해야 컴파일이 된다는 것에 유의하며 진행해보자.

// header1.h
{
    puts("Hello world!");
// header2.h
    return 0;
}
// main.c
#include <stdio.h>

int main(void)
#include "header1.h"
#include "header2.h"

언뜻보면 각 파일이 다 완성되지 않은 것 처럼 보인다.
여기서 main.c 파일을 보면 #include "header1.h"문장은 이 문장의 위치에 header1.h에 저장된 내용을 가져다 놓으라는 메시지를 선행처리기에 전달하는 것이다.
따라서, 아래 그림과 같이 작동하게 된다.

#include지시자는 파일의 내용을 단순히 포함시키는 용도로 사용된다.

헤더파일을 include 하는 방법

헤더파일을 include 하는 방법에는 두 가지가 있다.

<첫 번째 방법>
#include <헤더파일 이름>로 stdio.h, stdlib.h, string.h와 같은 표준 헤더파일을 포함시킬 경우 사용된다.

<두 번째 방법>
#include "헤더파일 이름"로 프로그래머가 정의하는 헤더파일을 포함시킬 때 사용하는 방식이다. 헤더파일 이름 부분에는 파일명 말고 경로를 넣어도 되는데 경로에는 두 가지 형식이 있다.
드라이브 명과 디렉터리 경로를 포함하는 절대 경로(완전 경로), 상위 디렉터리를 표현해주는 상대 경로다.
절대 경로로 헤더파일을 지정하게될 경우 다른 컴퓨터에서 컴파일 하는 경우 절대 경로가 완전히 동일해야 컴파일이 되기 때문에 꽤나 번거로워진다. 또한 운영체제가 달라지면 디렉터리의 구조가 달라지기 때문에 경로지정에 대한 부분을 전면 수정해야하는 번거로움이 있다.

#include "C:\CPoser\MyProject\header.h"	// Windows 상에서의 절대 경로 지정

반면에 상대 경로로 헤더파일을 지정하게 될 경우, 상위 디렉터리가 달라도 명시해준 디렉터리만 같으면 컴파일이 가능하기 때문에 시제로는 상대경로를 기반으로 헤더파일이 선언된다.

그렇다면 헤더파일에는 무엇을 담으면 좋을까?
외부에 선언된 변수에 접근하거나 외부에 정의된 함수를 호출하기 위한 선언들을 필요할 때마다 매번 삽입시키는 번거로움을 덜기 위해 이런 선언들을 헤더파일에 모아두고 필요할 때마다 헤더파일을 포함시킨다.
예제를 통해 이를 더 자세히 알아보자.

// basicArith.h
#define PI 3.1415
double Add(double num1, double num2);
double Min(double num1, double num2);
double Mul(double num1, double num2);
double Div(double num1, double num2);

// basicArith.c
double Add(double num1, double num2)
{
    return num1 + num2;
}

double Min(double num1, double num2)
{
    return num1 - num2;
}   

double Mul(double num1, double num2)
{
    return num1 * num2;
}    

double Div(double num1, double num2)
{
    return num1 / num2;
}

// areaArith.h
double TriangleArea(double base, double height);
double CircleArea(double rad);

// areaArith.c
#include "basicArith.h"

double TriangleArea(double base, double height)
{
    return Div(Mul(base, height), 2);
}

double CircleArea(double rad)
{
    return Mul(Mul(rad, rad), PI);
}

// roundArith.h
double RectangleRound(double base, double height);
double SquareRound(double side);

// roundArith.c
#include "basicArith.h"

double RectangleRound(double base, double height)
{
    return Mul(Add(base, height), 2);
}

double SquareRound(double side)
{
    return Mul(side, 4);
}

// main2.c
#include <stdio.h>
#include "areaArith.h"
#include "roundArith.h"

int main()
{
    printf("삼각형 넓이(밑변 4, 높이 2): %g \n", TriangleArea(4, 2));
    printf("원 넓이(반지름 3): %g \n", CircleArea(3));

    printf("직사각형 둘레(밑변 2.5, 높이 5.2): %g \n", RectangleRound(2.5, 5.2));
    printf("정사각형 둘레(변 3): %g \n", SquareRound(3));
    return 0;
}

> 출력
gcc .\main2.c
C:\Users\com\AppData\Local\Temp\cc0YWRqr.o:main2.c:(.text+0x22): undefined reference to `TriangleArea'
C:\Users\com\AppData\Local\Temp\cc0YWRqr.o:main2.c:(.text+0x68): undefined reference to `RectangleRound'
C:\Users\com\AppData\Local\Temp\cc0YWRqr.o:main2.c:(.text+0x86): undefined reference to `SquareRound'

VSC로 gcc해서 컴파일하는데 다른 파일에 있는 함수를 불러오지 못해서 오류가 발생한다...

해결 방안은 조만간 알아낼 것이다!!!

  • 헤더파일과 소스파일의 포함 관계

해결⭐

  • 오류 설명

    1. undefined reference to 'WinMain@16':
      이 오류는 Windows 환경에서 GUI 애플리케이션을 찾고 있는 경우 발생한다. main 함수가 제대로 정의되어 있어도, gcc가 -mwindows 옵션 없이 실행되면 이 오류가 발생할 수 있다.
    • 해결 방법: -mconsole 옵션을 추가하여 콘솔 애플리케이션으로 컴파일한다.
    1. undefined reference to 'IntDiv':
      이 오류는 IntDiv 함수가 정의된 intdiv.c 파일과 함께 컴파일하지 않았기 때문에 발생한다.
    • 해결 방법: 두 소스 파일을 함께 컴파일하여 링킹해 줘야 한다.
  • 해결

    gcc basicArith.c areaArith.c roundArith.c main2.c -o 원하는 파일명

    로 파일을 한번에 컴파일 하여 하나의 "원하는_파일명" 프로그램 실행 파일로 만들 수 있다.

  • 다른 해결 방법
    파일 수가 많거나 일일이 다 치기 귀찮을 땐 다른 방법도 하나 있다.
    Makerfile하나를 만들어서 한번에 컴파일 하는 것이다.

  • 출력

Triangle Area(base 4, height 2): 4 
Circle Area(rad 3): 28.2735 
Rectangle Round(base 2.5, height 5.2): 15.4
Square Round(side 3): 12
// 한글로 적다보니 컴파일이 잘 안되서 영어로 바꿔서 출력했다.

구조체 정의

구조체의 선언(typedef 선언)및 정의는 어디에 두는 것이 효율적일까?
소스파일 또는 헤더파일?
다음 예제를 통해 알아보자.

// intdiv.c
typedef struct div
{
    int quotient;   // 몫
    int remainder;  // 나머지
} Div;

Div IntDiv(int num1, int num2)
{
    Div dval;
    dval.quotient = num1/num2;
    dval.remainder = num1%num2;
    return dval;
}

// main3.c
#include <stdio.h>

typedef struct div
{
    int quotient;   // 몫
    int remainder;  // 나머지
} Div;

extern Div IntDiv(int num1, int num2);

int main()
{
    Div val = IntDiv(5, 2);
    printf("quotient: %d \n", val.quotient);
    printf("remainder: %d \n", val.remainder);
    return 0;
}

> 명령어 & 출력
> gcc .\intdiv.c .\main3.c -o example2
> .\example2.exe
quotient: 2 
remainder: 1

구조체는 다른 변수나 함수와 다르게 각 파일에서 선언 및 정의가 Div 구조체를 필요로 하는 모든 파일에 존재한다.
헤더파일을 만들어 이를 개선해보자.

//stdiv.h
typedef struct div
{
    int quotient;
    int remainder;
} Div;

// intdiv2.c
#include "stdiv.h"

Div IntDiv(int num1, int num2)
{
    Div dval;
    dval.quotient = num1 / num2;
    dval.remainder = num1 % num2;
    return dval;
}

// main4.c
#include <stdio.h>
#include "stdiv.h"

extern Div IntDiv(int num1, int num2);

int main()
{
    Div val = IntDiv(5, 2);
    printf("quotient: %d \n", val.quotient);
    printf("remainder: %d \n", val.remainder);
    return 0;
}

> 명령어 & 출력
> gcc .\intdiv2.c .\main4.c -o example3
> .\example3.exe
> quotient: 2 
remainder: 1 

구조체의 선언 및 정의는 헤더파일에 삽입하는 것이 좋다.

헤더파일의 중복삽입 문제

// stdiv.h
typedef struct div
{
    int quotient;
    int remainder;
} Div;

// intdiv3.c
#include "stdiv.h"

Div IntDiv(int num1, int num2)
{
    Div dval;
    dval.quotient = num1 / num2;
    dval.remainder = num1 % num2;
    return dval;
}

// intdiv3.h
#include "stdiv.h"
Div IntDiv(int num1, int num2);

// main5.c
#include <stdio.h>
#include "stdiv.h"
#include "intdiv3.h"

int main()
{
    Div val = IntDiv(5, 2);
    printf("quotient: %d \n", val.quotient);
    printf("remainder: %d \n", val.remainder);
    return 0;
}

> 명령어 & 출력
> gcc .\intdiv3.c .\main5.c -o example4
In file included from .\intdiv3.h:1:0,
                 from .\main5.c:3:
.\stdiv.h:1:16: error: redefinition of 'struct div'
 typedef struct div
                ^~~
In file included from .\main5.c:2:0:
.\stdiv.h:1:16: note: originally defined here
 typedef struct div
                ^~~
In file included from .\intdiv3.h:1:0,
                 from .\main5.c:3:
.\stdiv.h:5:3: error: conflicting types for 'Div'
 } Div;
   ^~~
In file included from .\main5.c:2:0:
.\stdiv.h:5:3: note: previous declaration of 'Div' was here
 } Div;
   ^~~

이 파일들의 헤더파일 포함관계를 살펴보면 아래 그림과 같다.

헤더파일 intdiv3.h가 stidiv.h를 포함하고 이씩 때문에 main.c에서 이를 한번 더 포함한다. 구조체 Div가 두 번 정의된 형태가 되어 컴파일 에러가 발생한다.
다음과 같은 유형의 선언은 여러번 삽입되어도 컴파일 오류가 발생하지 않는다.

extern int num;
void Increment(void);

조건부 컴파일을 활용한 중복삽입 문제의 해결

헤더파일의 중복삽입에 대한 해결책은 Chapter 26-3에서 학습한 '조건부 컴파일을 위한 매크로'에서 배웠었다.
다음 예제는 총 네 개의 파일로 이뤄진 구조에서 헤더파일의 중복삽입에 대한 해결책을 알 수 있다.

// stdiv2.h
#ifndef __STDIV2_H__
#define __STDIV2_H__

typedef struct div
{
    int quotient;
    int remainder;
} Div;

#endif

// intdiv4.h
#ifndef __INTDIV4_H__
#define __INTDIV4_H__

#include "stdiv2.h"
Div IntDiv(int num1, int num2);

#endif

// intdiv4.c
#include "stdiv2.h"

Div IntDiv(int num1, int num2)
{
    Div dval;
    dval.quotient = num1 / num2;
    dval.remainder = num1 % num2;
    return dval;
}

// main6.c
#include <stdio.h>
#include "stdiv2.h"
#include "intdiv4.h"

int main()
{
    Div val = IntDiv(10, 3);
    printf("quotient: %d \n", val.quotient);
    printf("remainder: %d \n", val.remainder);
    return 0;
}

#ifndef~#endif에 의해서 중복삽입으로 인한 문제가 발생하지 않도록 한다.


<Review>

드디어 C언어를 끝까지 다 했다!
와 이 책을 다 하게 될 줄 몰랐는데
사실 공부하면서 굳이 C언어를 왜 하냐라는 질문을 많이 받았었는데
나도 친구가 추천해줘서 시작을 했었다.
하지만 지금와서 이 이유에 대해 생각해봤을 때 컴퓨터의 흐름을 이해하면서 코딩을 배우기 가장 적합하고,
물론 나도 완전히 이해한 것은 아니지만 이 책 다음으로 열혈 자료구조를 통해서 조금 더 이해해보려고 한다.
나중에는 Java까지 배워서 3대 언어를 다 배워보고 싶다!

마지막 도전!프로그래밍을 통해서 정리하고 마무리해보려 한다.⭐

profile
백엔드 코린이😁

0개의 댓글