파일 뽀개기 ( # 친구들, 라이브러리)

CHOI·2021년 7월 17일
0

C 언어

목록 보기
24/28
post-thumbnail

헤더 파일

지금까지 헤더 파일에 함수의 원형만 넣었다. 그러나 헤더 파일에는 다음과 같은 것도 함께 주로 쓰는 경우가 대다수이다.(물론 헤더 파일에 일반 C 코드도 넣을 수 있지만 권장하지 않는다.)

  • 전역 변수
  • 구조체, 공용체, 열거형
  • 함수의 원형
  • 일부 특정한 함수 (인라인 함수.. 나중에 설명)
  • 매크로 (나중에 설명)

우리는 이중에서 3 가지만 지금 해보자. 나머지는 나중에 배워가면서 해보자.

먼저 구조체를 해보자. Human 이라는 구조체를 만들어보자. 이 구조체가 가질 정보는 사람의 이름, 나이, 성별이다. 성별은 열거형을 통해서 나타내고 이 구조체 변수에 대한 정보를 출력하는 함수와 이 구조체를 설명하는 함수가 필요하다.

Human 구조체를 보자

/* human.h */
enum { MALE, FEMALE };

struct Human {
  char name[20];
  int age;
  int gender;
};

struct Human Create_Human(char *name, int age, int gender);
int Print_Human(struct Human *human);

human.h 에는 위와 같은 것들이 포함되어 있다. 일단, 열거형을 통해 남자, 여자에 대한 정수 값이 선언되어 있고 Human 구조체 정의와 한 Human 구조체를 정의하는 함수 Create_Human 이 있고 한 Human 에 대한 정보를 출력하는 Print_Human 함수들이 설정되어 있다.

이제 이 함수들의 정보를 가지고 있는 human.c 파일을 보자

/* human.c */
#include <stdio.h>
#include "human.h"
#include "str.h"

struct Human Create_Human(char *name, int age, int gender) {
  struct Human human;

  human.age = age;
  human.gender = gender;
  copy_str(human.name, name);

  return human;
}
int Print_Human(struct Human *human) {
  printf("Name : %s \n", human->name);
  printf("Age : %d \n", human->age);
  if (human->gender == MALE) {
    printf("Gender : Male \n");
  } else if (human->gender == FEMALE) {
    printf("Gender : Female \n");
  }

  return 0;
}

일단 Human 구조체를 사용하니까 이 구조체에 대한 설명이 있는 human.h 와 기본적인 함수를 위한 stdio.h 그리고 copy_str 함수를 위한 str.h 헤더 파일 모두 include 하였다.

str.h 는 단순히 copy_str 을 위한 것이므로 아래와 같이 해주었다.

/* str.h */
char copy_str(char *dest, char *src);

str.c

/* str.c */
#include "str.h"

char copy_str(char *dest, char *src) {
  while (*src) {
    *dest = *src;
    src++;
    dest++;
  }

  *dest = '\0';

  return 1;
}

이와 같이 함수의 몸체를 정의했다. main 함수가 있는 test.c 는 다음과 같다.

#include <stdio.h>
#include "human.h"
int main() {
  struct Human Lee = Create_Human("Lee", 40, MALE);

  Print_Human(&Lee);

  return 0;
}

상당히 간단하다. 우리가 파일을 잘 나누었기 때문이다. 좋은 프로그램일 수록 main 함수에서 하는 일이 적어진다. 컴파일 해보면

실행 결과

Name : Lee
Age : 40
Gender : Male

이렇게 잘 나온다.

이제 파일을 분해하는 과정에 대해서 배웠으니 파일을 분해하는 습관을 들이도록 노력하자. 파일을 분할하게 되면 프로그래밍이 편해진다는 것을 느끼게 될 것이다. 각 소스 파일에 정확히 무엇을 나타내는지 표현하는 것 또한 중요하니까 잊지 말자.

라이브러리

이번에는 파일을 분해하는 것 만큼 중요한 것에 대해서 알아보자. 바로 '다른 사람이 만들어 놓은 함수들'을 사용하는 방법이다. 이렇게 다른 사람들이 만들어 놓은 것들을 가리켜서 라이브러리 라고 한다. 우리가 도서관에 가서 책을 고르듯이 C 언어에서는 우리가 원하는 함수를 라이브러리에서 찾아서 사용할 수 있다. 이는 정말 편리하지 않을 수 없다. 귀찮게 함수를 만들 필요가 없으니까.

아래 예제는 기존에 우리가 copy_str 을 이용하여 str1str2 를 복사하는 과정을 나타냈다.

/* test.c */
#include <stdio.h>
#include "str.h"
int main() {
  char str1[20] = {"hi"};
  char str2[20] = {"hello every1"};

  copy_str(str1, str2);

  printf("str1 : %s \n", str1);

  return 0;
}
/* str.h */
char copy_str(char *dest, char *src);
/* str.c */
#include "str.h"
char copy_str(char *dest, char *src) {
  while (*src) {
    *dest = *src;
    src++;
    dest++;
  }

  *dest = '\0';

  return 1;
}

실행 결과

str1 : hello every1

일단 위와 같이 복사가 잘 되는 것을 볼 수 있다. 그러나 정말로 귀찮은 일이 아닐 수 없다. 문자열을 복사하는 과정은 정말로 자주 사용될 것이다. 그런데 그때 마다 이와 같은 수고를 겪어야 한다면 정말 불편할 것이다. 그러나 다행스럽게도 개발자들은 이 역할을 함수를 '미리' 만들어 놓았다.

sring.h

/* 라이브러리의 사용 */
#include <stdio.h>
#include <string.h>
int main() {
  char str1[20] = {"hi"};
  char str2[20] = {"hello every1"};

  strcpy(str1, str2);

  printf("str1 : %s \n", str1);

  return 0;
}

실행 결과

str1 : hello every1

위와 같이 똑같이 나온다.

#include <string.h>

위 명령어는 string.h 파일에 있는 내용을 모두 가져다 붙인다 라는 의미를 가지고 있다. string.h 에는 '문자열을 다루는 함수들의 원형'이 모여 있다. 따라서 우리가 이 파일을 include 시킴으로써 문자열을 처리하는 여러가지 함수들을 사용할 수 있게 된다. 우리가 str.hinclude 해서 copy_str 을 사용할 수 있던 것과 일맥 상통한다. 우리는 여기서 strcpy 라는 함수를 사용했다.

strcpy(str1, str2);

이 함수는 copy_str 과 정확히 동일하게 str2str1 에 복사한다.

이렇게 사람들이 미리 만들어 놓은 함수들의 모임을 가리켜서 '라이브러리' 라고 한다. 우리가 현재 사용한 라이브러리는 문자열(string) 라이브러리 이다. 그렇다면 stdio.h 도 라이브러리일까? 맞다. 이는 입출력 라이브러리 로 입력과 출력에 관련된 함수들을 모아 놓았다. 대표적으로 printfscanf 가 있고 저번에 잠깐 나왔던 gether() 함수나 puts() 등등 수 많은 함수가 여기에 정의되어 있다. 자세한건 여기 를 참고하자

/* strcmp 함수 */
#include <stdio.h>
#include <string.h>
int main() {
  char str1[20] = {"hi"};
  char str2[20] = {"hello every1"};
  char str3[20] = {"hi"};

  if (!strcmp(str1, str2)) {
    printf("%s and %s is equal \n", str1, str2);
  } else {
    printf("%s and %s is NOT equal \n", str1, str2);
  }

  if (!strcmp(str1, str3)) {
    printf("%s and %s is equal \n", str1, str3);
  } else {
    printf("%s and %s is NOTequal \n", str1, str3);
  }

  return 0;
}

실행 결과

hi and hello every1 is NOT equal
hi and hi is equal

이번에는 strcmp 함수이다. 이 함수는 이전에 만든 compare 함수와 비슷한데 strcmp 함수는 두 문자열이 같다면 0 을 , 다르다면 0이 아닌 값을 리턴하게 되어 있다. 그래서 if 의 조건문이 !strcmp(str1, str2) 인 것이다.

이렇게 다른 라이브러리의 함수들을 이용하니까 상당히 편리하다. 지금까지 사용한 함수 말고도 다양한 함수가 라이브러리에 있으니까 찾아보도록 하자

# 친구들

우리는 여태까지 #include 라는 명령에 대해서 알아보았다. 이렇게 # 이 들어간 명령들은 전처리기 명령 이라고 하는데 '전처리기'의 의미 컴파일 이전에 쳐리된다는 뜻이다. 즉, 컴파일이 되기 이전에 #include 라는 부분은 #include 에 해당하는 파일의 소스 코드로 정확히 바뀐다.

# 이 들어간 명령어는 #include 말고도 #define , #ifdef 등등 많은 수의 명령어들이 있다. 이번에는 이러한 명령어들의 대해서 알아보자

#definde

/* #define */
#include <stdio.h>
#define VAR 10
int main() {
  char arr[VAR] = {"hi"};
  printf("%s\n", arr);
  return 0;
}

실행 결과

hi

배열의 크기를 정의할 때 변수를 사용할 수 없다. 그렇다면 위의 경우는 어떻게 문제 없이 실행 된 것일까?

#define 의 명령어는 다음과 같이 사용한다.

#define 매크로이름 값
// 전처리기 문들은 끝에 ; 를 붙이지 않습니다!!

이는 소스 코드에서 '메크로이름'에 해당하는 부분을 '값'으로 대체하게 된다. 물론 전처리기 명령이기 때문에 컴파일 이전에 정확하게 대체된다. 따라서

#include <stdio.h>
#define VAR 10
int main() {
  char arr[VAR] = {"hi"};
  printf("%s\n", arr);
  return 0;
}

이는 정확히 아래와 동일하다.

#include <stdio.h>
int main() {
  char arr[10] = {"hi"};
  printf("%s\n", arr);
  return 0;
}

이 작업이 컴파일 이전에 처리되기 때문에 컴파일러 입장에서는 arr[10] 이라는 문장을 처리한 것과 똑같으므로 오류를 내뿜지 않는 것이다.

#ifdef, #endif 조건부 컴파일

ifdefendif 는 이름에서 알 수 있듯이 뭔가 if 와 연관되어 있는 것 같다. 따라서 if 와 마찬가지로 어떠한 조건에 충족 되었을 때만 실행되는 것 같다.

/* ifdef */
#include <stdio.h>
#define A
int main() {
#ifdef A
printf("AAAA \n");
#endif
#ifdef B
printf("BBBB \n");
#endif
  return 0;
}

실행 결과

AAAA

만약 #define A#define B 로 바꾸면

실행 결과

BBBB

이렇게 나온다.

#ifdef 는 다음과 같은 형식으로 사용된다.

#ifdef /* 매크로 이름 */
/* (매크로 이름)이 정의되었다면 이 부분이 코드에 포함되고 그렇지 않다면 코드에
 * 포함되지 않는다. */
#endif

#ifdef#endif 는 언제나 짝을 지어서 사용하는데 #ifdef 에서 지정한 메크로가 정의되어 있다면 ifdefendif 속에 있는 코드가 포함되고 아니면 코드가 포함되지 않는 것으로 간주된다. #define A 를 통해 A 가 정의 되어 있으면

#ifdef A
printf("AAAA \n");
#endif

부분은 전처리기에 의해

printf("AAAA \n");

로 바뀌고

#ifdef B
printf("BBBB \n");
#endif

이 부분은 소스 코드에 포함되어 있지 않는 것으로 간주되어 컴파일러 입장에서 마치 주석처럼 무시된다.

#ifdef 를 사용하는 이유

이러한 기능이 도대체 왜 필요하냐고 물을 수 있지만 실제로 이러한 조건부 컴파일은 상당히 유용하다. 예를 들어서 계산기 프로그램을 만드는데, 계산기 모델 마다 조금씩 메모리와 CPU가 틀려서 어떤 계산기에는 double 형을 사용할 수 있지만 어떤 모델에서는 float 밖에 사용할 수 없다고 하자.

그러면 각각 이 계산기를 쓰기 위해 다음과 같이 소스를 짜야 할 것이다.

/*
계산기 모델 1 을 위한 코드
calculator1.c
*/
float var1, var2;
// do something
/*
계산기 모델 2 을 위한 코드
calculator2.c
*/
double var1, var2;
// do something

하지만 조건부 컴파일을 이용하면 이렇게 두 개로 나눠야 했던 작업을 다음과 같이 줄일 수 있다.

#define CACULATOR_MODEL_1

#ifdef CALCULATOR_MODEL_1
float var1, var2;
#endif
#ifdef CALCULATOR_MODEL_2
double var1, var2;
#endif;// do something

// do something

이때 define 되는게 무엇이냐에 따라서 간단히 무엇을 컴파일할지 나타낼 수 있다. 사실 ifdefendif 를 사용되는 경우는 이보다 더 많지만 일단 여기서 매듭을 짓겠다.

참고로 하나만 더 얘기 하자면 ifdef#else 라는 것을 사용하여 #ifdef 의 경우 이외의 나머지 것들을 처리하도록 나타낼 수 있다.

#ifdef CALC_1
// do something
#else
// do something 'else'
#endif

다음과 같이 말이다. 이 또한 역시 마지막에 #endif 를 빼먹으면 안된다.

또한 #ifdef 의 친구로 #ifndef 가 있는데, 이는 '매크로가 정의되어 있지 않으면'참이 된다. #ifdef 와 정 반대 개념이다. 이에 대해서는 나중에 큰 프로젝트를 진행하게 되면 차차 알아보자

profile
벨로그보단 티스토리를 사용합니다! https://flight-developer-stroy.tistory.com/

0개의 댓글