일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- set
- Stack
- Arduino
- 수광 소자
- Deque
- Visual Micro
- map
- 아두이노 소스
- 자료구조
- stl
- WinAPI
- 통계학
- html
- c++
- 운영체제
- Array
- vector
- arduino compiler
- 컴퓨터 그래픽스
- C언어
- priority_queue
- Algorithm
- 라인트레이서
- 시스템프로그래밍
- 아두이노 컴파일러
- 아두이노
- queue
- directx
- LineTracer
- list
- Today
- Total
Kim's Programming
C언어 - 함수(function)(2/4) 본문
헤더 파일
여때까지의 포스팅에서 저는 이렇게 말했습니다. ~~ 헤더에 들어가있는 ~~함수를 ~~~ 라고요. 왜 이렇게 말했을까요? 헤더에 함수가 있기 떄문입니다. 우선 함수의 원형을 알아보겠습니다. 함수의 원형을 알기위해서는 C컴파일러의 컴파일러 방식을 알아야합니다. 프로그래밍 언어는 해석방식에 따라 인터프리터 방식과 컴파일 방식으로 나누어지게되는데 컴파일러 방식이 훨씬 더 성능이 좋기 대부분이 컴파일 방식을 사용합니다. C언어도 물론 컴파일 방식을 사용합니다. 컴파일 방식이란 소스를 읽어서 기계어로 한꺼번에 번역하는 방식인데 번역을 몇번에 나누어 하냐에 따라 1패스, 2패스 등으로 구분을 합니다. 한 번 쭉 읽어서 번역을 다 해 내면 1패스(One Pas)방식이라고 하고 한번 읽어서 컴파일 준비를 한뒤 다시 읽으면서 기계어 코드로 바꾸는 방식은 2패스방식이라고 합니다. 2패스 방식의 대표적인 예가 어셈블러 입니다. 어셈블러는 소스상의 위치를 나타내는 레이블을 실제 번지로 바꾸기 위해 처음부터 끝까지 레이블의 번지를 미리 파악한 후 다시 처음부터 읽으면서 기계어 코드로 바꾸게 됩니다. 이러한 이유로 어셈블러는 2패스 방식이 아니라면 컴파일이 불가능 합니다.
3패스 이상의 언어들도 있지만 언어가 복잡할 수록 패스수는 늘어나며 메모리 또한 사용이 많아지고 속도는 더 떨어지게 되는 이유때문에 초기의 C언어는 컴파일 속도를 올리기 위해 1패스 방식은 많이 채택했습니다. C는 다른 언어보다 문법 구조가 복잡해서 컴파일 속도가 느린 편이러서 패스를 여러번 할 수 없었습니다. 이렇게 1패스 방식으로 작성되었기 떄문에 한번에 읽을 수 있는 장치가 마련될 필요가 있었습니다. 이러한 C언어의 1패스 방식 때문에 생긴것이 함수의 원형(Prototype). Prototype이란 함수에 대한 정확한 정보란 뜻이며, 리턴 타입, 함수 이름, 인수 리스트 등의 정보들로 구성되게 됩니다. 원형이라는 것이 왜 필요한지 알아보겠습니다.
이전 포스트에서 썼던 코드를 반대로 했습니다. 그 포스트와는 main함수와 print 함수를 바꾸었을 뿐 의미는 완전히 똑같습니다. 이 코드를 컴파일 하면 다음과 같은 오류가 뜨게 됩니다. 식별자를 찾을 수 없습니다(영어판에선 undeclared identifier이라고 뜹니다.) 1패스 방식으로 소스를 번역하기 떄문에 main 함수를 먼저 번역하게 되는데 이렇게 되면 print라는 것이 함수인지 변수인지 컴파일러가 인식을 하지 못하게 됩니다. 그렇기 떄문에 식별자가 찾을 수 없다고 오류를 띄우게 되는 것입니다. 이런 오류가 발생하는 이유가 바로 1패스 방식이기 떄문입니다. 소스는 위에서 아래로 한번만 읽어서 컴파일을 하기때문에 이러한 일이 발생합니다. 더 간단한 경우를 알아본다면 다음과 같은 경우도 있습니다.
1
2 |
i = 9;
int i = 0; |
cs |
다음과 같이 코드가 짜여 있으면 선언과 대입의 순서가 바뀌어 있기때문에 에러가 분명히 날 것입니다. 순서가 바꾸면 잘 되었을 겁니다. 하지만 만약 저렇게 식별자를 찾을 수 없을 때 다른 쪽에서 찾는 컴파일러를 만들수는 있겠지만 이런 컴파일러를 만들면 복잡하고 속도도 엄청 떨어지게 될 것입니다. 또한 엄청난 예외사항에 마주치게 될 것입니다. 문법은 편리보단 명료한 것이 더 좋습니다. 이런 문제가 발생하면 제일 좋은 방법은 두함수의 위치를 바꾸는 것입니다. main함수의 print()라는 것이 함수라는 것을 미리 알려주기 위해서 main함수보다 위에 선언해주면 되는 것입니다. 어떤 함수든지 사용하기 전에는 선언이 되어야합니다. 하지만 이렇게 하게되면 복잡한 프로그램을 쓰기엔 한계가 생기게 됩니다. 왜냐하면 함수가 100개 이상으로 가서 이리저리 서로서로 섞여 있으면 결국엔 복잡해 지고 순서도 너무 어렵게 되기 떄문입니다. 그래서 컴파일러가 이용할 함수들의 원형을 컴파일러가 미리 알 수 있도록 원형(Prototype)을 선언 하면 됩니다. 다음과 같은 코드를 보면서 원형이 어떤 것인지 살펴보도록 하겠습니다.
1
2
3
4
5
6
7
8
9
10
11
12 |
#include<stdio.h>
void print(); //원형(Prototype) 선언
void main()
{
print();
printf("출력은 잘 됐나요?\n");
}
void print()
{
printf("출력을 위한 함수입니다 \n");
} |
cs |
다음 코드를 보면 위에서는 컴파일이 정상적으로 되지 않던 코드와 다른 점이 보입니다. 2번째 줄에 있는 void print();라는 부분입니다. 컴파일러는 2번째 줄을 지나면서 전과는 달리 알게 됩니다. print()라고 되어 있는것은 void 형 함수라는 것을 미리 알려주는 것입니다. 물론 추가해서 인수가 있으면 인수도 같이 넣어줘야 합니다. C 컴파일러 1패스 방식을 사용하며 딱 한번만 읽어서 번역을 하기 떄문에 뒷부분에 나올 함수에 대한 정보를 미리 제공해야 하기 떄문입니다.
원형의 형식
함수의 원형은 컴파일러에게 함수에 대한 정보를 미리 제공하기 위해서 작성합니다. 그래서 함수의 본체(내용)는 적지 않으며 리턴 타입, 함수 이름, 인수목록만 적습니다. 함수를 정의하는 형식에서 본체를 빼고 뒤에 ;만 붙이면 그것이 바로 함수의 원형이 되게 됩니다. 원형 선언도 하나의 문장이기 때문에 꼭 뒤에 세미콜론을 붙여주어야합니다.만약 인수가 있을 경우 헷갈릴수 있으니 인수가 있는 경우로 가정해서 설명을 이어가겠습니다. 만약 int Gain(int a , int b) 라는 함수를 원형선언을 하기위해 위에 따르면 int Gain(int a, int b); 라고 선언하는 것이 맞습니다. 하지만 원형 선언또한 형식인수를 이용하기 떄문에 달라도 상관이 없습니다. 단지 형식과 이름만 일치하면 됩니다. 따라서 int Gain함수를 사용하기 위해서 int Gain(int aaaa, int bbbb)라고 선언해도 똑같은 형태로 인식합니다. 왜냐하면 형식인수이기 떄문입니다. 구형 컴파일러에서는 인수도 생략하기도 가능했다고 하지만 현재에는 다음과 같은 형태만 가능합니다. 원형의 형식은 다음과 같이도 선언이 가능합니다.
1
2 |
void Gain(int, int); //인수의 타입만 선언
void Gain(int a, int b); //형식 인수명 까지 선언 |
cs |
컴파일러는 형식인수의 이름에는 관심이 없습니다.단지 파악만 되면 되는 것입니다. 옛날에 메모리와 하드디스크 용량이 적었던 시절에나 한글자라도 줄이기 위해서 했겠지만, 지금은 용량이 충분하기 때문에 그런것은 상관이 없습니다. 단지 사용자가 구별하기 좋아야 하는 것입니다. 그래야 나중에 헷갈리거나 할 일이 줄어 들게 됩니다.
헤더 파일
앞에서 함수는 사용되기 전에 반드시 선어이 되어 있었습니다. 하지만 printf, scanf, 같은 함수는 어떻게 될까요? 표준함수들도 함수이기 떄문에 그 원형을 컴파일러가 알 수 있도록 하여야 합니다. 하지만 지금까지 한번도 선언 안했다고 오류가 뜬적은 없었는데요? 지금까지 했던 #include<stdio.h>라고 제일 앞에 선언 하는 것들이 원형을 가지고 있습니다. 표준 함수들은 컴파일러를 만들떄 미리 결정되어있기 떄문에 컴파일러와 함꼐 배포됩니다. 이처럼 표준 함수의 원형을 미리 작성 해 놓은 것을 헤더 파일(Header file)이라고 하며 stdio.h가 대표적인 헤더 파일입니다. #include 문은 헤더파일을 소스 안으로 불러오는 역할을 하므로 #include<stdio.h>라고 선언하는 것은 stdio.h에 선언된 모든 표준 함수를 사용가능하게 됩니다. stdio헤더를 열어보게되면 printf(), scanf() 같은 표준함수들이 원형으로 선언이 되어 있는데 복잡해 보이지만 엄청난 수의 표준헤더 들이 들어가있습니다.
방대한 양의 C함수들은 stdio헤더 뿐만 아니라 다른 헤더들에도 나누어 여러개로 나누어져 있다. 헤더와 함수들은 MSDN에 가서 예시와 설명을 찾아 볼 수 있습니다. 다음은 자주쓰는 헤더에 따라서 쓸수 있는 함수들 예시입니다.
헤더 파일 |
헤더 설명 |
포함 함수 |
stdio.h |
표준 입출력에 관한 함수 |
printf, scanf, puts |
conio.h |
키보드 및 화면 입출력 |
getch, cprint |
stdlib.h |
자료변환 함수 |
malloc, free |
math.h |
수학함수 |
sin, cos, log |
string.h |
문자열 조작 함수 |
strcopy, strlen |
헤더파일과 함수에 대해서 특징 몇개를 알아보겠습니다.
- 제일 중요한 정보는 표준함수의 원형입니다. 이 원형이 헤더파일에 선언되어 있기 떄문에 #include문으로 헤더 파일만 포함하면 표준 함수를 사용 할 수 있습니다.
- 각 표준 함수들이 사용하는 매크로 상수들을 정의합니다.
- 각 표준 함수들이 사용하는 열거형 타입을 정의합니다.
- 자료의 가공을 간편하게 해 주는 매크로 함수들을 정의한다.
- 구조체, 공용체 등 표준 함수가 요구하는 사용자 정의 타입을 정의한다.
추가로 C나 C++ 모두 헤더 파일은 통산 확장자 h 를 가집니다. 하지만 최신 C+표준은 헤더 파일에 별도의 확장자를 붙이지 않는 것으로 변경되었습니다. 기존의 C언어 헤더파일들은 확장자를 제거하는 대신 앞에 C를 붙임으로써 C언어로 부터 물려받은 헤더임을 표시하도록 하였습니다. stdio.h -> cstdio, string.h -> cstring, math.h->cmath 등으로 이름이 바뀌었습니다. 물론 호환성 유지를 위해 위에와 같이 .h가 붙은 헤더도 사용이 가능합니다. 하지만 표준이 완성이 된 상태가 아니라 h확장자가 빠진 것도 이용이 가능하고 아직 헤더파일명을 인식 못하는 컴파일러도 있습니다.
모듈
함수를 사용하기 전에 원형만 선언하면 함수의 본체는 어디에 있던지 상관이 없습니다. 컴파일러가 이미 함수에 대한 정보를 확보하고 있으므로 호출부 보다 앞에 있어도 호출부 보다 뒤에 있어도 상관이 없습니다. 심지어 다른 소스 파일에 함수의 본체를 둘 수 있습니다. 예를 들면 스타크래프트라는 게임을 간단히 나타낸다고 했을때 프로그램안에 캐릭터 그래픽, 소리, 등등을 나타내야 할 것 입니다. 그렇게 하여 starcraft 라는 메인 에 grapic이라는 헤더와 sound라는 헤더를 이용하여 따로만든 graphic.cpp, sound.cpp에서 함수들을 끌어와 쓰는 경우가 가능합니다. 이런식으로 하나의 실행 파일을 만들기 위한 소스를 여러 개로 나누어 개발하는 방식을 모듈 분할 컴파일(또는 다중 모듈 컴파일) 방식이라고 합니다. 모듈 분할 컴파일 방식은 다음과 같은 이점이 있습니다.
- 컴파일 속도가 빠릅니다. 함수들이 여러 개의 모듈에 분산되어 있으므로 한 함수를 수정한 후 컴파일할 떄 이 함수가 속한 모듈만 컴파일하면 됩니다. 예를 들어 위에 처럼 만들게 되면 sound 모듈만 수정을 했을떄는 나머지 모듈들을 다시 컴파일 할 필요 없이 sound 모듈만 컴파일 하면 됩니다. 모듈을 잘게 나눌수록 컴파일 속도는 더욱더 빨라집니다.
- 분담 작업이 가능합니다. 각자 파트를 나눌수 있는 것입니다. 한 사람이 다 필요 할 필요가 없고 모듈마다 각 사람이 붙어서 하나를 만들어 낼 수도 있다. 하지만 단일 모듈 방식일떄는 두명의 사람이 하나의 소스를 작성하게 되면 매우 비효율 적이고 서로간의 코딩 버릇 또한 다르기 때문에 이는 작업이 아주 어렵게 됩니다.
- 프로젝트 관리가 쉽습니다. 모듈별로 관련 함수를 모아 놓기 때문에 특정부분에서 오류가 생기게 되면 그 모듈만 수정해서 손보면 되기 때문에 버그 처리도 간단합니다. 하지만 한 프로젝트에 모든 모듈들을 떄려 박았다면 찾기는 찾기도 힘들고 관리도 힘들 것입니다. 사실 제가 2~3만줄 되는 코드를 살피면서 버그 찾을 거 생각하니 소름돋습니다;;
- 모듈 재 사용이 가능합니다. 모듈은 기능적으로 독립적인 함수의 집합을 구성하기 떄문에 다른 프로젝트에 쉽게 재사용이 가능합니다. 만약 sound라는 모듈이 어디에나 쓸 수 있게 훌륭하게 제작이 되었다면 이 모듈은 다른데에 그대로 활용이 가능할 것 입니다.
'Programming > C' 카테고리의 다른 글
C언어 - 함수(function)(4/4) (0) | 2015.08.07 |
---|---|
C언어 - 함수(function)(3/4) (0) | 2015.08.05 |
C언어 - 함수(function)(1/4) (0) | 2015.07.30 |
C언어 - 연산자(2/2) (1) | 2015.07.22 |
C언어 - 연산자(1/2) (0) | 2015.07.20 |