함수

old/C Grammer 2010. 11. 17. 15:57

1. 함수의 구성 원리

1) 함수

사용자 정의 함수를 만드는 기본 형식은 다음과 같다.

 type name(인수 목록)
{
          함수의 본체
}

name : 함수의 이름이며 이 이름을 통해 함수를 호출한다. 함수의 이름도 명칭(Identifier)이므로 명칭을 만드는 규칙대로 기억하기 쉽고 의미를 잘 표현할 수 있는 이름을 주는 것이 좋다. 점수를 출력하는 함수라면 PrintScore, 게임을 끝내는 함수라면 EndGame, 합계를 구하는 함수라면 GetSum 같은 이름을 붙이면 된다.

인수 목록 : 함수가 해야 할 일의 세부 사항을 지정하며 함수의 작업거리라고 할 수 있다. 함수는 고유의 기능을 가지고 있고 호출부에서는 이 기능을 사용하기 위해 함수를 호출하는데 이때 함수에게 일을 시키기 위해서는 작업에 필요한 값을 전달해야 한다. 함수의 동작에 필요한 인수는 없을 수도 있고 여러 개일 수도 있는데 인수 목록에 필요한 인수의 타입과 이름을 밝힌다.

예를 들어 점수를 화면으로 출력하는 PrintScore 함수의 경우 출력할 현재 점수가 몇점인가를 가르쳐 주어야 하며 이런 정보가 인수로 전달된다. 만약 점수값 하나만 인수로 전달받는다면 PrintScore(int Score) 식으로 점수를 전달받을 것이다. printf는 서식 문자열과 출력할 값을 인수로 전달받으며 gotoxy는 이동 좌표를, delay는 지연시간을 전달받는다. 인수는 필요한만큼 사용할 수 있으며 개수의 제한은 없다. 필요하다면 점수를 출력할 좌표나 점수의 출력 형태 등도 인수로 전달받을 수 있다.

type : 함수가 리턴하는 값의 데이터형이며 함수의 작업 결과라고 할 수 있다. 함수는 고유의 작업을 실행하고 그 결과를 호출원에게 다시 돌려 준다. 예를 들어 합계를 구하는 함수 GetSum은 자신이 구한 합계를 호출원에게 보고하는데 이때 돌려주는 값의 타입이 바로 함수의 타입이다. 정수형 값을 리턴한다면 int, 실수형 값을 리턴한다면 double이라고 타입을 써 준다. getch 함수는 입력된 문자값을 리턴하며 wherex, wherey는 커서 좌표를 조사한다. 단순히 어떤 기능만 수행하는 함수라면 리턴하는 값이 없을 수도 있는데 이런 함수를 void 함수라고 한다.

본체 : { } 괄호안에 실제 함수의 코드가 위치한다. 이 블록 안에 함수의 고유 기능을 수행하는 코드를 작성하면 된다. PrintScore 함수의 본체에는 인수로 전달된 점수값을 printf 함수로 출력하는 코드가 작성될 것이다.

2) 인수

인수(Parameter)는 호출원에서 함수에게 넘겨주는 작업 대상이라고 할 수 있다. 두 함수 사이의 정보 교환에 사용되므로 매개 변수(Argument)라고도 한다. Max 함수의 경우 두 정수값 중 큰 값을 선택하는 기능을 하므로 대상이 되는 두 정수값을 전달받아야 하며 그래서 int a, int b를 인수 목록에 적어 주었다. 다음은 여러 가지 함수의 인수 예이다.

(x,y) 위치에 점수 Score를 출력하는 함수 : PrintScore(int x, int y, int Score)
실수 x의 제곱근을 구하는 함수 : GetSqrt(double x)
문자열에서 공백의 개수를 조사하는 함수 : GetSpaceNum(char *str)

작업 대상을 따로 전달받을 필요가 없다면 인수를 전혀 사용하지 않을 수도 있다. 예를 들어 화면을 지우는 clrscr 함수는 어떤 동작을 할 것인지 이미 정의되어 있으며 화면을 지우는데 별다른 지시 사항이 없으므로 별도의 인수를 전달받지 않는다. 인수가 없는 함수는 인수 목록을 비워 두거나 아니면 인수를 받지 않는다는 것을 분명히 표시하기 위해 인수 목록에 void라고 적는다.

 clrscr();
clrscr(void);

 인수는 형식인수와 실인수로 구분되는데 함수의 인수 목록에 나타나는 인수를 형식 인수라고 하며 함수 호출부에서 함수와 함께 전달되는 인수를 실인수라고 한다.

형식 인수는 호출원에서 전달한 실인수값을 잠시 저장하기 위한 임시 저장 장소이므로 어떤 이름이든지 사용해도 상관없다. 자신에게 전달된 인수값을 형식 인수에 잠시 대입해 놓고 함수 본체에서는 형식 인수로 호출원으로부터 전달된 실인수값을 읽기만 하면 된다. Max 함수는 다음과 같이 작성해도 완전히 동일하다.

int Max(int num1, int num2)
{
     if (num1 > num2) {
          return num1;
     } else {
          return num2;
     }
}
 
호출원에서 전달받은 값을 이 함수내에서 num1, num2로 부르겠다는 뜻이다. Max 함수가 이렇게 정의되어 있고 main 에서 Max(a,b)를 호출했다면 형식 인수 num1이 실인수 a의 값을 가지게 되고 형식인수 num2는 실인수 b의 값을 가지게 될 것이다. 인수 전달은 일종의 대입 연산이며 함수 호출 과정에서 a, b의 값이 num1, num2로 대입된다. Max 함수는 전달받은 형식인수값의 대소를 판단하여 더 큰 수를 리턴하기만 하면 된다. 다음 예제는 두 정수의 값을 더하는 함수 Add를 정의한 것이다.

3) return

인수가 호출원으로부터 전달되는 작업 대상이라면 리턴값은 함수가 호출원으로 돌려주는 작업 결과이다. 앞에서 이미 봤지만 함수가 결과를 리턴할 때는 return 문을 사용한다. return문의 기능은 다음 두 가지이다.

우선 가장 일반적인 기능은 함수의 결과값을 호출원으로 돌려주는 것이다. return 예약어 다음에 리턴하고자 하는 값을 써 준다. Max 함수의 경우 a, b값의 대소에 따라 return a나 return b 명령으로 a나 b의 값 중 하나를 리턴하며 Add 함수는 ttt와 ddd를 더한 결과값을 리턴한다.

함수는 자신에게 맡겨진 임무대로 계산을 수행하여 그 결과를 호출원으로 리턴하며 호출원에서는 함수가 리턴한 값을 함수의 결과값으로 취한다. 함수가 어떤 값을 리턴할 것인가는 함수의 타입으로 이미 정의되어 있으므로 미리 정해진 타입의 값을 리턴하면 된다. Max와 Add는 모두 정수형 값을 리턴하도록 정의되었다.

호출원에서는 타입만 맞다면 함수가 리턴하는 값을 곧바로 사용할 수 있다. 즉, int 타입이 올 수 있는 곳이라면 int 타입을 리턴하는 함수도 항상 올 수 있다. Max 함수는 정수값을 리턴하므로 m=Max(a,b) 구문으로 Max 함수가 리턴하는 값을 정수형 변수 m에 대입할 수 있으며 Add 함수가 리턴하는 정수값을 printf의 %d 서식과 대응시킬 수도 있다.

함수는 단 하나의 유일한 결과를 리턴하기 때문에 타입만 맞다면 수식내에서도 바로 사용할 수 있다. d=Add(Max(a,b),c) 식은 a와 b중 큰 값과 c를 더한 값을 d에 대입한다. Max가 리턴하는 값이 정수형이므로 이 함수의 호출 결과를 Add 함수의 첫 번째 인수로 사용할 수 있고 Add가 더한 값을 정수형 변수 d에 대입할 수 있다. 함수는 한 번에 하나의 리턴값만 돌려줄 수 있기 때문에 타입만 맞다면 수식내에서 함수를 바로 쓸 수 있는 것이다. 다음 함수 호출도 모두 적법하다.

gotoxy(Max(a,b),Add(c,d));
Add(Add(Add(Add(1,2),3),4),5);

두 번째로 return 문은 결과를 돌려주는 것 외에 함수를 강제 종료시키는 기능을 한다. 함수 실행중에 return문을 만나면 함수의 뒷부분이 남아있건 말건 무조건 함수를 종료하고 호출원으로 돌아가 버린다. return문이 함수의 결과값을 돌려주는 명령인데 결과값을 돌려 주는 시점이 함수의 임무가 끝난 시점이므로 더 이상 함수의 나머지 부분을 실행할 필요가 없다.

4) void형

함수는 작업한 결과를 리턴값으로 돌려줄 수 있는데 모든 함수가 리턴값을 가지는 것은 아니다. 리턴할 값이 없는 함수도 있는데 이런 함수를 void형 함수라 한다. 정수형 값을 리턴하면 정수형 함수, 실수형 값을 리턴하면 실수형 함수 등으로 함수가 리턴하는 값의 데이터형이 곧 함수의 타입(type)이 되는데 void형 함수는 아무런 값도 리턴하지 않는 함수다.

단순히 "삐" 하는 효과음을 낸다든가, 미리 정해진 메시지를 출력한다든가 하는 함수들은 특별히 호출원으로 돌려줄 값이 없다. 이런 함수를 정의할 때는 함수 타입에 void라고 적는다. void형 함수는 내부적으로 작업만 할 뿐이지 계산 결과를 리턴하지 않으므로 호출원에서는 함수를 호출만 하며 리턴값을 대입받거나 사용하지 말아야 한다.

5) 함수의 다른 이름

함수(function) : 특정 계산을 수행하며 리턴값이 있다. 반드시 수식내에서만 사용할 수 있으며 함수 단독으로 문장을 구성할 수 없다. 이 경우는 수학적 의미의 함수와 거의 유사하므로 적합한 용어 사용예라 할 수 있다.

프로시저(procedure) : 특정 작업을 수행하며 리턴값이 없다. 리턴값이 없기 때문에 수식내에서는 사용할 수 없으며 단독으로 문장을 구성할 수는 있다. C의 void 함수가 이에 해당한다.

 C의 함수는 파스칼의 함수와 프로시저에 해당하는 특성을 모두 가진다. 리턴값이 있을 수도 있고 없을 수도 있으며 모든 함수는 단독으로 사용할 수 있다. 파스칼은 형태에 따라 함수와 프로시저를 엄격하게 구분하여 실수를 방지하지만 C언어는 특별한 구분이 없으므로 융통성이 있다. 다음은 다른 언어와 구분되는 C 함수의 특징들이다.

함수끼리는 서로 평등한 관계에 있으며 상호 수평적이다. 즉, 함수끼리 언제나 호출 가능하다는 뜻이며 한 함수가 다른 함수에 예속되지 않는다. 반면 파스칼은 함수 내부에 지역 함수를 정의할 수 있어 함수끼리 수직적인 계층을 이룰 수 있다.

함수 중에서 가장 기본이 되는 함수를 main이라고 하며 프로그램의 시작점이 된다. 그러나 main이 다른 함수들과 특별히 다르지는 않으며 다만 프로그램의 실행 시작점이라는 것만 조금 특수할 뿐이다.

리턴값은 있을 수도 있고 없을 수도 있다. 리턴값이 있는 함수는 리턴 타입을 가지며 그렇지 않은 함수는 void형으로 선언하면 된다.

항상 단독으로 문장을 구성할 수 있다. 리턴값이 없는 함수는 단독으로만 사용할 수 있고 리턴값이 있는 함수는 수식 내에서 쓸 수도 있고 단독으로 쓸 수도 있다. 값을 리턴하는 함수라고 해서 반드시 리턴 값을 대입받거나 수식내에서만 써야 하는 것은 아니며 리턴값을 버리는 것이 가능하다. printf함수도 출력한 문자의 개수를 리턴하며 에러 발생시 음수를 리턴하는데 에러가 발생하는 경우가 거의 없고 출력 길이에도 특별한 관심이 없으므로 보통 단독으로 사용된다.

값에 의한 호출 방식을 사용한다. 실인수가 형식인수에 대입될 때 항상 값이 대입된다는 뜻이다. 호출 방식에 대해서는 잠시 후 따로 연구해 볼 것이다.


2. 헤더 파일

1) 함수의 원형

함수의 원형을 이해하기 위해서는 C 컴파일러의 컴파일 방식에 대해 알아야 한다. 프로그래밍 언어는 해석 방식에 따라 인터프리터 방식과 컴파일 방식으로 나누어지는데 컴파일 방식이 훨씬 더 성능이 좋기 때문에 대부분의 언어가 컴파일 방식을 사용한다. C언어도 물론 컴파일 방식을 사용한다.

컴파일 방식은 소스를 읽어 기계어로 한꺼번에 번역하는 방식인데 번역을 몇 번에 나누어 하느냐에 따라 1패스, 2패스 등으로 구분된다. 한 번 죽 읽어서 번역을 다 해 내면 1패스 (one pass)방식이라고 하며 한 번 읽어서 컴파일 준비를 한 후 다시 읽으면서 기계어 코드로 바꾸는 방식을 2패스 방식이라고 한다. 2패스 방식의 대표적인 예가 어셈블러인데 소스상의 위치를 나타내는 레이블을 실제 번지로 바꾸기 위해 처음부터 끝까지 소스를 읽어 레이블의 번지를 미리 파악한 후 다시 처음부터 읽으면서 기계어 코드로 번역한다. 어셈블러는 이런 식으로 소스를 두 번 읽지 않으면 컴파일이 불가능하다.

3패스 방식을 채택하는 언어도 있고 디스 어셈블러들은 5패스 방식까지도 사용한다. 언어가 복잡해질수록 패스 수가 늘어나며 사용하는 메모리는 많아지고 컴파일 속도는 떨어진다. 초기의 C 컴파일러들은 컴파일 속도를 높이기 위해 통상 1패스 방식을 많이 채택했었다. C는 다른 언어보다 문법 구조가 복잡해서 컴파일 속도가 느린 편이라 패스를 여러 번 할 수가 없었다. 그래서 C 소스는 컴파일러에 의해 읽히는 족족 기계어로 번역된다.

물론 최근의 C컴파일러들은 1패스가 아닌 것들도 있지만 초기의 컴파일러들이 1패스 방식으로 작성되었기 때문에 C표준은 이런 컴파일러를 위해 한 번에 소스를 읽을 수 있는 장치를 마련할 필요가 있었다. 이러한 C의 1패스 방식 때문에 함수의 원형이라는 것이 필요하다. 원형(ProtoType)이란 함수에 대한 정확한 정보라는 뜻이며 리턴 타입, 함수 이름, 인수 리스트 등의 정보들로 구성된다.

원칙적으로 함수는 사용되기 전에 미리 그 형태를 컴파일러가 알 수 있도록 해야 하는데 그 방법이 바로 원형(ProtoType)을 선언하는 것이다. 함수의 원형을 미리 선언해 두면 본체는 뒤쪽에 있더라도 함수 호출부에서 이 명칭이 함수이고 어떤 인수를 요구한다는 것을 미리 알 수 있게 된다. 이렇게 함수의 정보를 미리 컴파일러에게 알려 주는 것을 "원형을 선언한다"라고 한다.

int Max(int a, int b);               // 원형 선언

void main() {

2) 원형의 형식

함수의 원형은 컴파일러에게 함수에 대한 정보를 제공하기 위해 작성한다. 그래서 함수의 본체는 적지 않으며 리턴 타입, 함수 이름, 인수 목록만 적는다. 함수를 정의하는 형식에서 본체를 빼고 뒤에 세미콜론을 붙이면 이것이 함수의 원형이다. 원형 선언 자체도 하나의 문장이므로 세미콜론은 반드시 있어야 한다.

함수를 정의하는 형식에서 함수 이름이 포함된 첫 번째 줄만 옮겨 적고 뒤에 세미콜론을 붙이면 이것이 함수의 완전한 원형이다. Max 함수의 완전한 원형은 int Max(int a, int b);가 된다. 이때 함수의 원형에 적는 형식 인수의 이름은 사실 아무 의미가 없다. int Max(int kkk, int mmm); 이라고 써도 결과는 완전히 동일하다. 왜냐하면 형식인수란 함수의 본체내에서 호출원으로부터 전달된 값을 참조하기 위해 사용하는 것인데 원형 선언은 본체를 가지지 않으며 형식 인수를 사용하지 않기 때문이다.

그러나 인수들의 타입은 아주 중요한 의미가 있다. int Max(int a, int b); 원형은 이 함수의 이름이 Max이고 정수형 값을 리턴한다는 것 외에 두 개의 정수를 인수로 취한다는 정보를 담고 있다. 컴파일러는 이 원형을 미리 기억하고 있다가 혹시 사용자가 Max(1, 3.14) 같이 실수를 전달하면 소수점 이하가 잘린다는 경고를 출력하거나 Max(2,"name") 같이 변환할 수 없는 인수일 경우는 에러로 처리한다. 즉, 원형 선언이란 컴파일러에게 "혹시 내가 실수를 하면 좀 알려 줘"하고 요청해 놓는 것이다.

함수의 원형에서 형식 인수의 이름은 의미가 없기 때문에 형식 인수를 생략하는 간략한 원형 선언 방식도 허용된다. 인수 리스트에서 형식 인수 이름은 빼 버리고 인수의 타입만 적는 방식인데 Max 함수의 간략한 원형은 int Max(int,int);가 된다. 이 원형만으로도 컴파일러는 Max 함수에 대한 모든 정보를 다 알 수 있다.

이 외에 구형 C 컴파일러는 인수 리스트를 생략하는 int Max(); 같이 더 간략화된 형식도 지원했었으나 요즘의 C++ 컴파일러들은 인수 리스트의 생략은 허용하지 않는다. 다음에 배우게 되겠지만 C++은 인수 목록이 다르면 다른 함수로 취급하는 다형성을 지원하기 때문이다. 그래서 함수의 원형은 다음 두 가지 형식으로 쓸 수 있다.

int Max(int a, int b);          // 완전한 원형 - 형식 인수명도 적어준다.
int Max(int, int);               // 간략한 원형 - 인수의 타입만 밝힌다.

어떤 방식으로 원형을 선언하는가에 따른 차이점은 거의 없다. 어차피 컴파일러는 형식 인수의 이름 따위에는 전혀 관심이 없기 때문이다. 그래서 메모리나 하드 디스크값이 비쌌던 과거에는 간략한 형식의 원형을 많이 사용했었고 요즘도 이런 형식을 즐겨 쓰는 사람들이 많다. 의미도 없는 형식 인수명을 일일이 타이프하는 것도 꽤 성가신 일이기 때문이다.

하지만 최근에는 가급적이면 함수의 원형을 완전하게 적는 것이 더 권장된다. 형식 인수의 이름이 컴파일러에게는 아무 도움이 되지 않지만 함수를 사용하는 사람에게는 도움이 되기 때문이다. 예를 들어 화면에 원을 그리는 DrawCircle이라는 함수가 있다고 하자. 아마 이 함수의 원형은 다음과 같이 작성될 것이다.

void DrawCircle(int x, int y, int radius);

별다른 설명이 없더라도 이 원형으로부터 첫 번째, 두 번째 인수가 원의 중심점이고 세 번째 인수가 반지름이라는 것을 직관적으로 알 수 있다. 만약 간략한 형태로 void DrawCircle(int, int, int); 와 같이 써 버리면 이 함수를 사용하는 사람은 함수의 본체를 보거나 레퍼런스를 참조해야만 인수들의 정확한 의미를 알 수 있을 것이다. 원형에 포함된 형식 인수의 이름은 주석보다 더 좋은 참고 정보가 되므로 가급적이면 인수의 의미를 정확하게 전달할 수 있는 이름을 적어 놓는 것이 좋다.

표준 함수의 원형은 사용자에게 가급적이면 많은 정보를 제공하기 위해 모두 완전한 형태로 작성되어 있는 경우가 많다. 컴파일러에 따라서는 간략한 원형을 사용하는 것도 있다. 사용자 정의 함수들도 특별한 이유가 없는 한 완전한 형태로 적는 것이 좋다.

3) 헤더 파일

표준 함수의 원형을 미리 작성해 놓은 것을 헤더 파일(Header File)이라고 하며 stdio.h가 대표적인 헤더 파일이다. #include문은 헤더 파일을 소스 안으로 읽어 들이는 역할을 하므로 #include <stdio.h>명령에 의해 stdio.h에 선언된 모든 표준 함수의 원형을 공짜로 선언할 수 있게 된다.

4) 모듈

각각의 기능을 구현하는 함수 집합에 대해 모듈을 따로 만들 수 있으며 각 모듈은 개별적으로 컴파일되어 링커에 의해 하나의 실행 파일로 합쳐진다.

모듈 분할 컴파일 방식의 장점
 (1) 컴파일 속도가 빠르다. 모듈을 작게 나눌 수록 컴파일 속도는 빨라진다.
 (2) 분담 작업이 가능하다. 각 기능별로 분담이 가능하다.
 (3) 프로젝트 관리가 쉽다. 소스 관리가 쉽다.
 (4) 모듈을 재사용할 수 있다. 다른 프로젝트에 공유가 가능하다.

5) 모듈 예제

#include <Turboc.h>

void BoxMessage(char *str);

void main()
{
     BoxMessage("박스를 그리고 그 안에 문자열을 출력한다.");
     BoxMessage("전달된 문자열의 길이에 적당한 박스를 스스로 계산한다.");
     BoxMessage("신기하군");
} 

void BoxMessage(char *str)
{
     int i;
     int len;   
   
   
 len=strlen(str);
     
 puts("");

     for (i=0;i<len+4;i++) {
          putch('-');
     }
     puts("");

    
printf("| %s |\n",str);
     for (i=0;i<len+4;i++) {
          putch('-');
     }
     puts("");
}



3. 함수 호출 방식

인수란 호출원에서 함수에게 일을 시키기 위한 정보인데 인수를 어떻게 전달하는가에 따라 값 호출(call by value) 방식과 참조 호출(call by reference) 방식이 있다. 인수를 넘기는 방식에 따라 실인수의 값이 변경되는가 아닌가의 차이점이 있다. 다소 어려울 수도 있지만 함수를 이해하는데 아주 중요한 내용이므로 잘 알아 두도록 하자.

1) 값 호출 (call by value)

먼저 값 호출에 대해 알아보자. 값 호출 방식이란 실인수의 값이 형식 인수로 전달되는 방식이다. 다음 예제의 plusone 함수는 하나의 정수값을 전달받아 그 값에 1을 더한 값을 리턴한다.

j=plusone(i);

int plusone(int a)
{
     a=a+1;
     return a;
}

값을 복사해서 넘긴다라고 생각하면 쉽겠다.


2) 참조 호출 (call by reference)

값 호출과 참조 호출의 또 다른 차이점은 실인수로 상수를 전달할 수 있는가 하는 점이다. 값 호출 방식은 값을 전달하기 때문에 plusone(5)와 같이 상수를 실인수로 사용할 수 있다. i든 k든 5나 3이든 값을 가지기만 하면 형식인수 a가 이 값을 대입받을 수 있다. 심지어 i*k+1같은 수식도 계산된 후에는 값으로 평가되므로 이 값을 전달할 수 있다. 그러나 참조 호출은 번지를 전달하기 때문에 번지를 가지는 변수만 사용할 수 있으며 상수는 사용할 수 없다. 상수는 메모리를 점유하고 있지 않기 때문에 번지가 없다. 즉, 상수는 좌변값이 아니며 참조 호출 함수의 실인수로는 좌변값만 사용할 수 있다.

만약 plusref(&5)와 같이 상수의 번지를 넘기고 싶다고 해 보자. 5라는 상수는 좌변값이 아니므로 &5라는 표현부터가 벌써 잘못된 것이다. 5는 메모리에 저장된 값이 아니므로 번지가 없고 상수 5는 어디까지나 상수 5일 뿐이지 어떤 방법을 쓰더라도 이 값은 6이나 4가 될 수는 없다.

plusref(&i);

void plusref(int *a)
{
    *a=*a+1;
}

3) 출력용 인수

 입력용 인수 : 가장 일반적인 의미의 인수이며 호출원에서 함수에게 작업 대상을 지정하기 위해 전달된다. plusone 함수의 a인수, printf 함수의 모든 인수들, gotoxy 함수의 인수들이 모두 입력용 인수이다. 함수의 입장에서 볼 때 이 인수들은 호출원으로부터 입력되는 값이므로 입력용 인수라고 한다.

gotoxy(10,20);
plusone(i);
printf("The Result is %d",k); 

이 호출문들에서 사용된 모든 인수가 바로 입력용 인수들이다. 함수로 입력되는 실체는 값이므로 상수를 바로 쓸 수도 있다. 하지만 초기화되지 않은 변수를 쓰는 것은 일반적으로 경고 처리되는데 다음 호출문을 보자.

int i,j;
j=plusone(i);

i를 선언만 하고 바로 plusone 함수에게 이 값을 전달했다. i를 선언만하고 초기화하지 않았으므로 이때 plusone이 받게 되는 값은 쓰레기값이며 컴파일러에 의해 경고로 처리된다. 알지도 못하는 값에 대해 어떤 동작을 하려고 했으므로 정상적인 코드가 아니라는 뜻이며 이대로 내버려 두면 골치아픈 버그의 원인이 되기도 한다. 입력용 인수는 함수로 전달되기 전에 반드시 원하는 값으로 초기화되어야 한다.

 출력용 인수 : 참조 호출로 전달되는 인수들의 대부분이 출력용 인수이다. 함수에게 작업거리를 주기 위해 전달되는 것이 아니라 함수가 작업한 결과를 돌려 받기 위해서 사용된다. 함수의 입장에서 볼 때 호출원에서 입력되는 값이 아니라 호출원으로 출력되는 값이므로 출력용 인수라고 한다. 대표적인 예는 scanf 함수의 인수이다.

int i;
scanf("%d",&i); 

정수값 하나를 입력받기 위해 i변수를 선언하고 이 변수의 번지를 scanf 함수에게 전달했다. 참조 호출이기 때문에 상수는 사용할 수 없고 반드시 변수만 사용할 수 있다. 또한 입력되는 값이 아니므로 초기화할 필요가 없으며 위 코드는 경고없이 정상적으로 컴파일된다. scanf는 i의 값을 입력받아 어떤 동작을 하는 것이 아니라 먼저 어떤 동작을 한 후 그 결과를 i에 대입하기 위해 이 변수의 번지를 전달받았다. 그래서 scanf의 인수는 출력용이다.

함수가 호출원으로 작업 결과를 돌려주는 일반적인 방법은 리턴값을 사용하는 것이지만 이런 식으로 출력용 인수를 사용하여 리턴값을 돌려줄 수도 있다. 리턴값은 단 하나밖에 돌려줄 수가 없지만 출력용 인수를 사용하면 여러 개의 값을 동시에 돌려줄 수 있다는 이점이 있다. 예를 들어 게임의 캐릭터 좌표를 조사하는 함수를 만든다면 다음과 같이 만들어야 한다.

void GetCharacterPos(int *x, int *y);

평면상의 좌표는 (x,y) 두 개의 정수값으로 표현되기 때문에 리턴값으로는 이 좌표를 돌려줄 수 없으며 참조 호출로 두 개의 정수값을 돌려 주도록 해야 한다. 함수의 리턴값이 하나밖에 없는 이유는 유일한 값을 리턴해야만 함수를 수식내에서 바로 사용할 수 있기 때문이다. 이 예에서처럼 여러 개의 값을 동시에 조사해야 하는 경우라면 출력용 인수를 사용하거나 아니면 구조체를 리턴하도록 해야 한다. 이 함수를 호출하려면 반드시 두 개의 변수를 선언해야 하며 수식내에서 바로 사용할 수도 없다.

 입출력용 인수 : 이 인수는 입력과 출력을 겸하는 인수이다. 즉, 호출원에서 함수에게 작업 대상을 전달하기 위해서도 사용하며 함수가 호출원에게 작업 결과를 전달하기 위해서도 사용한다. 출력을 해야 하므로 이 인수는 참조 호출 방식으로 전달되어야 하며 형태상으로는 출력용 인수와 동일하다.

출력용 인수와 다른 점은 입력을 겸하고 있기 때문에 반드시 초기화를 해야 한다는 점이다. 그러나 초기화하지 않고 전달할 경우 컴파일러가 이를 에러나 경고로 처리하지는 않는데 왜냐하면 이 인수가 출력용인지 입출력용인지는 형태상으로 구분되지 않기 때문이다. 하지만 초기화되지 않은 쓰레기값을 사용하면 당연히 원하는 결과는 나오지 않는다.

참조 호출 방식이므로 상수는 물론 사용할 수 없으며 변수만 사용할 수 있다. 입출력용 인수의 예는 그리 흔하지는 않지만 바로 앞에서 만들어 보았던 plusref 함수의 a 인수가 바로 입출력을 겸하는 인수이다. 이 함수는 a의 값을 입력으로 받아 들이기도 하며 이 값을 1 증가시켜 다시 a로 돌려 주기도 한다. 입력, 출력을 모두 겸하고 있으니 입출력용 인수라고 하는 것이다.


3. 전처리기 (PreProcessor)

1) #include

컴파일러는 #include 명령을 먼저 처리하여 헤더 파일을 모두 포함한 후에 컴파일을 시작한다. 그래서 #include <stdio.h> 명령을 소스 선두에 작성해 놓으면 stdio.h에 선언된 함수의 원형, 데이터 타입, 열거 상수 등을 공짜로 사용할 수 있는 것이다.

#include 명령을 쓰는 대신 헤더 파일의 내용을 복사하여 직접 소스에 붙여 넣어도 결과는 동일하지만 불편할 것이다. #include 명령으로 포함시키면 하나의 헤더 파일을 여러 소스에서 동시에 사용할 수 있으며 소스의 길이가 짧아져 관리하기에도 좋다. #include 다음에 포함할 파일의 이름을 적는데 사용하는 괄호에 따라 두가지 형태가 있다. 

#include <file.h> : C에서 제공하는 표준 헤더파일을 포함시키고자 할 때 < > 괄호를 사용한다. 컴파일러의 옵션중에 표준 헤더 파일들이 어떤 디렉토리에 있는지를 기억하는 옵션이 있는데 비주얼 C++의 경우 도구/옵션/프로젝트/VC++ 디렉토리(6.0의 경우 Tools/Options/Directories) 대화상자에서 헤더 파일 디렉토리를 지정한다. < >괄호를 사용하면 표준 헤더 파일 디렉토리에서 지정한 파일을 찾는다.

#include "file.h" : 사용자가 직접 작성한 헤더 파일을 포함시키고자 할 때 " "괄호를 사용한다. 이 괄호를 사용하면 소스 파일과 같은 디렉토리에서 헤더 파일을 먼저 찾아 본다. 직접 만든 헤더 파일은 보통 소스와 같은 디렉토리에 두므로 이 괄호를 사용하면 된다.

괄호 형식에 따라 헤더 파일을 어디서 먼저 찾을 것인가의 검색 순서가 달라지는데 사실 두 괄호는 그다지 엄격하게 구분할 필요가 없다. " " 괄호를 사용했더라도 현재 디렉토리에 이 파일이 없으면 표준 헤더 파일 디렉토리도 검색하며 반대로 < > 괄호를 사용했더라도 현재 디렉토리도 같이 검색한다. 즉 #include "stdio.h"라고 할 경우 현재 디렉토리에 stdio.h가 있는지 보고 없다면 표준 헤더 파일 디렉토리도 검색하므로 결과는 마찬가지다.

괄호에 따라 결과가 달라지는 경우는 표준 헤더 파일 디렉토리와 현재 디렉토리에 같은 이름을 가지는 헤더 파일이 있을 경우인데 어떤 파일이 우선적으로 포함되는가만 다를 뿐이다. 이런 특수한 경우가 아니라면 굳이 괄호를 구분할 필요는 없다. 그러나 관행상 표준 헤더 파일은 < > 괄호를, 사용자 정의 헤더 파일은 " "를 사용하고 있으므로 이 관행을 지키는 것이 바람직하다.

#include 명령은 주로 헤더 파일을 포함시키기 위해 사용하지만 꼭 헤더 파일만 가능한 것은 아니다. 확장자가 cpp인 파일도 포함할 수 있으며 txt나 임의의 파일이라도 텍스트 파일이기만 하면 다 포함할 수 있다. 예를 들어 1000줄쯤 되는 아주 큰 배열 정의문이 있는데 이 정의문이 너무 길어 소스를 편집하기가 불편하다면 이 부분만 array.cpp로(또는 array.txt, array.inc) 따로 떼어 내고 주 파일에서는 #include "array.cpp"로 불러 오면 된다.

#include 다음의 파일명은 대소문자를 구분하지 않는다. C언어 자체는 대소문자를 구분하지만 윈도우즈의 파일 시스템이 대소문자를 구분하지 않기 때문에 #include <STDIO.H>라고 쓸 수도 있고 #include <stdio.h>라고 해도 문제가 없다. 물론 유닉스나 리눅스 환경에서는 대소문자가 구분되므로 가급적이면 원래 파일명과 똑같이 쓰는 것이 좋다.

#include 명령은 중첩 가능하다. 포함한 파일이 다른 파일을 포함하고 있다면 포함된 모든 파일이 주 파일로 읽혀진다. 예를 들어 A가 B를 포함하고 있는 상태에서 주 파일이 #include "A" 명령을 사용하면 A가 B까지 같이 가지고 주 파일에 포함된다.

2) #define

#define전처리문은 매크로 상수를 정의하는데 기본 문법은 다음과 같다.

#define 매크로명 실제값

#define GAMETIME 240

기억하기 쉬운 적당한 이름을 주고 실제값을 뒤에 써 주면 되는데 예를 들어 YEAR라는 매크로 상수를 365로 정의하고 싶다면 #define YEAR 365라고 정의하면 된다.

다음은 #define 문으로 매크로 상수를 정의할 때의 일반적인 주의 사항이다. 상식적으로 쉽게 이해될 것이다.

#define문은 전처리문이지 코드를 생성하는 명령이 아니다. 그래서 행 끝에 세미콜론은 붙이지 않는다. #define MACH 1200.0;이라고 써서는 안된다. 만약 이렇게 쓰면 세미콜론조차도 매크로의 실제값에 포함되어 버린다. #include는 물론이고 다음에 배울 모든 전처리문들도 세미콜론은 붙이지 않으며 주석을 제외한 다른 문장이 뒤따라 올 수 없다.

매크로의 이름도 일종의 명칭이기 때문에 명칭 규칙에 맞게 작성해야 한다. 중간에 공백이 들어간다거나 숫자로 시작한다거나 다른 명칭과 충돌해서도 안된다. 매크로 상수는 다른 명칭과 구분될 수 있도록 관습적으로 대문자를 사용하는 것이 일반적이다. NUM, MY_MACRO 등은 적법한 명칭이며 3RD는 숫자로 시작되었으므로 부적격하며 MY MACRO는 중간에 공백이 있으므로 매크로명으로 사용할 수 없다.

매크로 이름에는 공백이 들어갈 수 없지만 매크로의 실제값은 공백을 가질 수 있다. #define 전처리문은 매크로를 실제값으로 단순 치환할 뿐이므로 공백이 있건 한글을 사용하건 전혀 상관하지 않는다. 다음은 자주 쓰는 메시지 문자열을 매크로로 정의한 것이다.

#define ERRMESSAGE "똑바로 하란 말이야"

문자열을 통째로 ERRMESSAGE라는 매크로 상수로 정의했으며 좀 어색해 보이지만 적법한 문장이다. 실제 코드에서 puts(ERRMESSAGE); 형식으로 사용하면 된다.

문자열 상수내에 있는 매크로나 다른 명칭의 일부로 포함된 경우는 치환되지 않는다. #define HUMAN 5 라는 전처리문은 소스의 모든 HUMAN을 찾아 5로 치환할 것이다. 그러나 printf("I am a HUMAN"); 문장에 있는 HUMAN은 매크로 상수와 이름은 같지만 어디까지나 문자열일 뿐이므로 치환하지 않는 것이 합리적이다. HUMANNAME이라는 다른 명칭에도 HUMAN이라는 이름이 포함되어 있지만 분리된 명칭이 아니므로 5NAME으로 치환되지 않는다.

매크로는 중첩 가능하다. 즉, 매크로 상수가 매크로 상수를 참조할 수 있다는 얘기다. 다음 예를 보자. 

#define A 3
#define B (A*2) 

A를 3으로 정의했고 B는 A에 2를 곱한 값으로 정의했으므로 6이 될 것이다. B가 A를 참조하고 있는 셈이다. 두 번 세 번 얼마든지 중첩할 수 있되 단 중첩되는 매크로가 먼저 정의되어 있어야 하므로 순서에 신경써야 한다.

값을 가지지 않는 빈 매크로도 정의할 수 있다. 

#define PROFESSIONAL 

이 매크로는 값을 가지지 않으며 매크로 상수 자체만 존재할 뿐이다. 이렇게 값을 가지지 않는 매크로는 주로 조건부 컴파일 지시자와 함께 사용되며 존재하는가 아닌가만으로 의미를 가진다. 또한 아무 값도 가지지 않음을 명시할 때도 빈 매크로를 정의한다.

#define 전처리문의 동작이 단순하기 때문에 매크로 상수는 쉽게 이해가 갈 것이다. 그렇다면 매크로 상수를 어느 수준으로 사용할 것인가에 대해 생각해 보자. 모든 상수를 다 매크로 상수로 정의한 후 사용할 수도 있고 적당한 수준에서 상수를 직접 사용할 수도 있는데 이는 개인적인 취향에 따라 결정할 문제이다.

매크로 상수는 분명히 기억하기 쉽도록 해 주고 일괄적인 수정에 도움을 준다. 그러나 너무 남발하면 오히려 소스를 더 읽기 어렵게 만들 수도 있으며 기억력 보조에도 별 도움이 되지 못하는 경우가 있다. 다음 예를 보자.

3) 매크로 함수

#define을 이용하여 다음과 같이 매크로 함수를 작성할 수는 있지만 컴파일이 진행될 때 각 매크로를 함수의 내용으로 그대로 치환하는 형식이라 효율적인 방법은 아니다.

#define dubae(i) i+i;
#define VALUE2 VALUE+100;
#define printmsg(x,y,str) { gotoxy(x,y); puts(str); }

4) C 프로그램의 구조

C프로그램은 대체로 다음과 같은 구조를 가진다고 했다. 물론 필요에 따라 순서를 조금씩 바꿀 수 있겠지만 여러 사람들이 오랫동안 C로 프로그래밍을 해 본 결과 이 구조가 가장 이상적이라는 것을 알게 되었으며 그래서 대부분의 C 소스는 이런 구조로 만들어진다. 처음 배울 때는 남들 하는대로 일단 따라하는 것이 좋다.

#include <...>
#define ...

함수의 원형
전역변수

 void main()
{
     코드
}

함수
함수
함수

 제일 먼저 헤더 파일을 포함하는 #include 문이 오고 다음으로 매크로를 정의하는 #define이 온다. 그리고 이 프로그램이 사용하는 함수의 원형과 전역변수를 선언하고 main 함수가 가장 먼저 오며 main 함수 다음에 사용자 정의 함수를 작성한다. 이 방식대로 코드를 작성하면 제일 말썽이 없고 코드를 유지 보수하기에 편리하다.

출처: http://winapi.co.kr/

'old > C Grammer' 카테고리의 다른 글

표준 함수  (0) 2010.11.19
기억 부류  (0) 2010.11.18
연산자  (0) 2010.11.16
제어문  (0) 2010.11.15
변수  (0) 2010.11.12
Posted by jazzlife
,