앞에서 C언어에 대한 기본적인 사항을 설명 했으니 이제 함수에 대한 이야기를 해야겠습니다.
모든 C프로그램은 함수로 시작해서 함수로 끝이 납니다.
반드시 함수를 사용합니다.
함수를 활용하는 좀 더 자세한 내용은 다른 글에서 설명 할 것입니다. 여기에서는 함수에 대한 일반적이고 기본적인 것을 알아 보도록 하겠습니다.
프로그램이 문제 해결을 위한 전체적인 작업이라고 한다면 함수는 그 전체 작업을 이루는 세부 작업 또는 기능이라고 할 수 있습니다. 그의 이름을 불러 주었을 때 나에게로 와서 꽃이 되었다는 김춘수님의 시에서처럼 함수는 호출할 때 의미를 갖습니다. 같은 작업을 해야 하는 것을 매번 반복해서 작성한다는 건 매우 위험한 방법입니다. 중간에 값이 변한다면 작성된 모든 로직을 찾아 바꿔야 합니다. 하지만 한 번 작성하여 이름을 불러 사용할 수 있다면 효과적일 겁니다. 그리고 중간에 작은 기능이 바뀌더라도 한 곳만 바꿔주면 호출될 모든 곳이 바뀐 기능대로 적용될 겁니다. 예를 들어 라면 끓이기의 경우 라면을 끓이는 전체 작업을 하나의 큰 덩어리로 생각할 수도 있지만 라면 끓이는 작업 중 물 끓이는 작업을 따로 떼어 놓고 생각하면 커피를 마실 때, 스파게티를 만들 때, 등등 여러 가지로 활용할 수 있다는 것입니다. 이 자리를 빌어 삼가 고(故) 김춘수 시인의 명복을 빕니다.
처음에 프로그램을 작성하게 되면 거의 모든 사람들이 함수 하나로 프로그램을 작성합니다. 평생 프로그램 하나만 만들 생각인 것처럼 말이죠. 하지만 그것은 결코 좋은 방법이 아닙니다. 다음 번에 물 끓이기와 같은 동작이 또 필요하게 되면 결국 그 부분의 소스를 통째로 복사해서 붙입니다. 바로 Copy & Paste의 인생이 시작된 것입니다. 여러 군데 소스를 통째로 복사해오면서 여기 저기 삐걱대기 시작합니다. 일명 짜깁기 소스죠. 짜깁기 소스의 가장 큰 문제점은 하나를 고치면 다른 곳에서 열두 개의 새로운 버그가 발생한다는 것입니다. 때문에 점점 고치기가 두려워집니다. 바로 Bug's Life의 시작입니다. 저도 수없이 많은 세월을 짜깁기로 보냈습니다. 스스로 나는 프로그래머가 아니라 공장에서 조립하는 사람인가 생각하기도 했습니다. 어렵고 힘들겠지만 부디 처음부터 함수를 나눠서 작성하는 연습을 하기 바랍니다.
강의를 하다 보면 함수 하나로 작성했다가 완성되면 나중에 나누려고 하는 사람들 꼭 있습니다. 하지만 완성되어 함수로 나누려고 보면 문제가 굉장히 심각해집니다. 그러니 몇 번 시도하다가 포기합니다.
부탁입니다. 어렵고 힘들겠지만 함수로 나눠서 작성하는 연습을 하기 바랍니다. 매도 먼저 맞는게 낫다고 합니다만, 사실 매는 안맞는 게 최곱니다.
기본적으로 Input logic(입력), Business logic(실제 순수 작업), Presentation logic(출력) 이 세가지를 분리하는 것이 좋습니다. 키보드로 입력하던 방법이 터치스크린으로 바뀌더라도 Business logic에는 아무런 영향을 주지 않아야 합니다. 텍스트 모드로 출력하던 방법이 고객의 요구로 GUI환경으로 바뀌었다고 Business logic까지 뜯어 고쳐야겠습니까? 물론 이게 정답은 아닙니다. 상황에 따라 적절히 섞일 수도 있습니다. 모든 문제를 해결하는 유일한 마스터키 같은 존재는 없습니다.
그러면 일반적인 C언어의 함수는 어떤 모양일까요? 다음과 같이 함수의 프로토타입과 함수의 본체로 구성됩니다.
리턴타입 함수명(인자타입들…) ;
매우 있어보이게 유식한 말로 하면 함수의 프로토타입이라고 합니다. 쉽게 말하면 함수를 사용하겠다는 함수의 선언입니다. 요즘은 컴파일러가 똑똑해져서 프로토타입을 따로 선언하지 않아도 잘 동작하기도합니다만 프로토타입을 선언하지 않으면 경고나 에러를 내는 컴파일러가 여전히 존재합니다. 부디 적어주길 바랍니다.
함수 선언, 함수의 프로토타입들을 따로 모아 놓은 것이 바로 헤더 파일입니다. 우리가 사용해왔던 printf(), scanf()함수들도 역시 사용하거나 부르기 전에 함수 선언을 해야 하는데 우리는 그 동안 한번도 선언해준 적이 없지 않습니까? 그건 바로 stdio.h 헤더 파일에 모두 들어있기 때문입니다. 그 파일을 포함하라는 선행처리문법이 바로 #include <stdio.h>이었습니다.
리턴타입 함수명(인자타입 인자명[, 다른인자타입들 다른인자명들] ) {
연산자와 예약어로 구성된 C문장들이 여기에 옵니다 ;
알고리즘을 C언어로 작성합니다 ;
C문장은 반드시 semicolon으로 끝나야 합니다 ;
행 번호는 사용하지 않으며 {로 시작해서 }로 끝납니다;
// C++스타일의 line 주석입니다. 라인 끝까지 주석입니다.
/* 순수 C 스타일의 block 주석입니다. */
/*
보통 주석은 선행처리기에 의해 모두 삭제되어 사라지므로
컴파일러는 주석이 있는 지조차 모릅니다.
하지만 멋진 프로그래머일수록 주석을 잘 작성합니다.
어떤 사람은 주석이 소스의 절반을 차지 하기도 합니다.
알고 있겠지만 주석이 없는 프로그램은 그 존재 자체가 죄악입니다.
이렇게 C 함수는 예약어와 연산자 그리고 주석으로 구성됩니다.
*/
// block 주석을 사용할 때 다음과 같은 수식을 주의해야 합니다.
// result = data /*pointer // 이것은 data를 *pointer로 나누는 식입니다.
// 하지만 /와 *가 만나 block 주석이 시작하는 것으로 인식됩니다.
}
이것이 실제 함수의 모양새입니다. '리턴타입'은 함수를 실행한 결과 값의 타입을 말합니다. 리턴 값이 없다면 반드시 void 라고 적어야 합니다. 리턴 타입을 생략하면 int형이 생략된 것입니다. 단 프로토타입에서는 int 리턴 타입을 생략할 수 없습니다.
인자들은 함수를 실행할 때 필요한 데이터들을 전달하기 위해 사용합니다. 물론 함수 내부에서 직접 선언해서 사용할 수도 있지만 함수 내부에서 선언하게 되면 함수가 실행될 때 마다 같은 값으로 실행될 것입니다. 예들 들자면 면을 뽑아내는 기계를 하나 만들었습니다. 보통은 재료를 넣어주는 곳이 있고 면이 뽑아져 나오는 곳이 있습니다. 내부야 어떻게(어떤 알고리즘으로) 구성되어 있든 적절한 재료를 넣어주게 되면 면이 뽑아져 나옵니다. 쌀가루와 물을 넣으면 쌀 국수가 나오고 메밀가루와 물을 넣으면 메밀 국수가 나올 것 입니다. 재료를 넣어 주는 곳이 바로 함수의 인자와 같은 역할을 합니다. 뽑아져 나오는 국수가 바로 리턴 타입이 됩니다. OO가루를 넣으면 OO국수가 나온다는 것입니다.
그런데 다른 사람은 가루 넣는 곳을 만들지 않고 기계 안에서 자체적으로 가루를 만들게 했습니다. 그렇게 되면 이 기계는 항상 같은 국수만을 만들어 낼 것입니다. 물론 다른 재료를 안에서 만들어 사용하는 국수 만드는 기계를 또 copy & paste 해서 만들면 되겠지만 불편하죠? 그래서 보통 함수들은 리턴 값과 인자를 갖도록 되어있습니다. 가끔 인자만 없는 게 아니라 아예 리턴 값도 없는 경우도 있습니다. 국수를 만들어서 아예 그 기계 안에서 다 처리하는 거죠. 우습죠?
보통 인자가 없을 때는 그냥 생략하면 됩니다. 아무것도 적지 않으면 void가 생략 된 것입니다. 사실 아주 특별한 경우를 제외하고 void 리턴 값과 void 인자의 함수는 저는 프로그램 못 짭니다 라고 밝히는 것과 같습니다. 인자와 리턴 값을 적절히 잘 사용하는 기법은 다른 글들에서 설명할 것 입니다. 반드시 기억할 것은 인자는 0개 또는 한개 이상을 가질 수 있지만 리턴 값은 없거나 한 개뿐입니다. 물론 OOP언어로 넘어가게 되면 같은 함수가 여러 다른 값을 리턴 하는 다형성을 지원하기도 합니다. 하지만 C언어는 함수 하나에 리턴 값 하나, 이것이 원칙입니다.
그러면 그 동안 우리가 사용했던 printf()나 scanf()도 함수라는 것은 알 수 있습니다. 이렇게 만들어진 함수는 어떻게 사용할까요?
리턴값을저장할변수 = 함수명(인자로사용될변수들) ; /* 리턴 값을 사용할 때 */
함수명(인자로사용될변수들) ; /* 리턴 값을 사용하지 않을 때 */
함수는 호출할 때 반드시 함수 프로토타입에 적은 인자들과 같은 타입의 인자와 개수를 사용해야 합니다. 하지만 주의 하셔야 합니다. 함수를 부르는데 대가를 꼭 지불해야 합니다. 너무 잦은 함수 호출은 프로그램의 진행을 느리게 만듭니다. 왜 재귀호출 함수가 느려지는지 한번 고민해 보길 바랍니다. 그러나 그럼에도 불구하고 함수를 나눠서 작성해야 하는 이유가 있습니다.
함수는 표준함수와 컴파일러 제공 함수 그리고 사용자 정의 함수로 구성이 되어있습니다. 표준함수라 함은 국제적으로 약속된 함수로 반드시 모든 컴파일러가 제공해야 합니다. 또한 각 컴파일러는 컴파일러 자신만을 위한 함수도 함께 제공하고 있습니다.
그리고 이러한 표준함수나 컴파일러 제공 함수를 사용하여 프로그래머가 문제 해결을 위해 직접 작성한 함수를 사용자 정의 함수라고 합니다.