지금까지 헤더 파일에 함수의 원형만 넣었다. 그러나 헤더 파일에는 다음과 같은 것도 함께 주로 쓰는 경우가 대다수이다.(물론 헤더 파일에 일반 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
을 이용하여 str1
에 str2
를 복사하는 과정을 나타냈다.
/* 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
일단 위와 같이 복사가 잘 되는 것을 볼 수 있다. 그러나 정말로 귀찮은 일이 아닐 수 없다. 문자열을 복사하는 과정은 정말로 자주 사용될 것이다. 그런데 그때 마다 이와 같은 수고를 겪어야 한다면 정말 불편할 것이다. 그러나 다행스럽게도 개발자들은 이 역할을 함수를 '미리' 만들어 놓았다.
/* 라이브러리의 사용 */
#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.h
를 include
해서 copy_str
을 사용할 수 있던 것과 일맥 상통한다. 우리는 여기서 strcpy
라는 함수를 사용했다.
strcpy(str1, str2);
이 함수는 copy_str
과 정확히 동일하게 str2
를 str1
에 복사한다.
이렇게 사람들이 미리 만들어 놓은 함수들의 모임을 가리켜서 '라이브러리' 라고 한다. 우리가 현재 사용한 라이브러리는 문자열(string) 라이브러리 이다. 그렇다면 stdio.h
도 라이브러리일까? 맞다. 이는 입출력 라이브러리 로 입력과 출력에 관련된 함수들을 모아 놓았다. 대표적으로 printf
와 scanf
가 있고 저번에 잠깐 나왔던 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
등등 많은 수의 명령어들이 있다. 이번에는 이러한 명령어들의 대해서 알아보자
/* #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
는 이름에서 알 수 있듯이 뭔가 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
에서 지정한 메크로가 정의되어 있다면 ifdef
와 endif
속에 있는 코드가 포함되고 아니면 코드가 포함되지 않는 것으로 간주된다. #define A
를 통해 A
가 정의 되어 있으면
#ifdef A
printf("AAAA \n");
#endif
부분은 전처리기에 의해
printf("AAAA \n");
로 바뀌고
#ifdef B
printf("BBBB \n");
#endif
이 부분은 소스 코드에 포함되어 있지 않는 것으로 간주되어 컴파일러 입장에서 마치 주석처럼 무시된다.
이러한 기능이 도대체 왜 필요하냐고 물을 수 있지만 실제로 이러한 조건부 컴파일은 상당히 유용하다. 예를 들어서 계산기 프로그램을 만드는데, 계산기 모델 마다 조금씩 메모리와 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
되는게 무엇이냐에 따라서 간단히 무엇을 컴파일할지 나타낼 수 있다. 사실 ifdef
와 endif
를 사용되는 경우는 이보다 더 많지만 일단 여기서 매듭을 짓겠다.
참고로 하나만 더 얘기 하자면 ifdef
도 #else
라는 것을 사용하여 #ifdef
의 경우 이외의 나머지 것들을 처리하도록 나타낼 수 있다.
#ifdef CALC_1
// do something
#else
// do something 'else'
#endif
다음과 같이 말이다. 이 또한 역시 마지막에 #endif
를 빼먹으면 안된다.
또한 #ifdef
의 친구로 #ifndef
가 있는데, 이는 '매크로가 정의되어 있지 않으면'참이 된다. #ifdef
와 정 반대 개념이다. 이에 대해서는 나중에 큰 프로젝트를 진행하게 되면 차차 알아보자