프로그래밍/C

[C언어 강좌-12] 함수 (Function)

Peter Ahn 2018. 5. 26. 20:29
반응형


안녕하세요 피터입니다.

오늘은 C언어의 함수 (Function)에 대해 알려드리겠습니다.


개요

수학에서는 함수 이런식으로 표기합니다.


y = f(x)

여기에서 x 는 함수의 입력값이 되고, y 출력값이 됩니다.


C언어에서 함수(Function)도 이와 유사하다고 볼 수 있습니다.

입력값을 가질 수 있으며, 이에 대응되는 값을 출력할 수 있습니다.



문법(Syntax)

함수 선언 (Function Declaration)

함수는 다음과 같이 선언할 수 있습니다.


DataType FunctionName(DataType, ...);


위와 같은 문장(문장이기 때문에 ; 세미콜론으로 끝납니다)을 함수 선언 또는 함수 원형이라고 합니다.

맨 앞쪽에 있는 DataTypeReturnType이며 함수의 출력값입니다.

FunctionName에서 함수 이름을 지정할 수 있으며, 함수 이름 뒤의 ( ) 안에는 0개 이상매개변수(Parameter)를 넣을 수 있습니다.

각각의 매개변수들은 , (콤마)로 구분하며 개수의 제한은 없습니다.

다만 너무 많은 매개변수를 넣으면 사용하기가 힘들겠죠.


함수 이름을 정하는 것은 변수 이름을 정하는 것 만큼이나 굉장히 중요합니다.

이름은 고유한 함수의 식별자로 방대한 소스코드 내에서 코드의 흐름(Flow)를 추적하는데 있어서 이정표와 같은 역할을 하기 때문입니다.


때문에 함수 이름이나 변수 이름을 대충 지었다가는 여러분의 코드를 보는 사람들로 하여금 코드의 미로에 갇혀 헤매이게 만들지도 모릅니다.

그러니 함수 이름을 정할 때는 의미가 잘 전달될 수 있도록 신중하게 결정해주시기 바랍니다.


올바르게 함수명과 변수명을 짓는 방법에 대해서는 추후에 포스팅하겠습니다.


함수 선언문 내에는 실제로 동작에 관련된 코드가 들어있지 않기 때문에 함수를 선언(Declaration)했다면 반드시 함수를 정의(Definition)해야합니다.

즉, 선언과 정의가 쌍을 이뤄야 합니다.


선언과 정의를 함수의 헤더(Header)바디(Body)라고도 부릅니다.


선언만 하고 정의를 않은 상태에서 해당 함수를 호출하게 되면 컴파일러에서 Link Error를 선물해줄겁니다.


이렇게 헤더와 바디를 구분해놓는 이유는 라이브러리의 경우 실제 동작 코드는 컴파일이 완료된 lib 파일이나 dll 파일 안에 존재하는데, 함수를 호출하기 위해서는 먼저 컴파일러(Compiler)에게 함수 원형을 알려줘야 합니다.

때문에 라이브러리 함수를 호출하기 전에 해당하는 함수의 원형을 선언해줘야 하는데 라이브러리에 포함된 함수가 한두개가 아니기 때문에 일일히 적기가 곤란합니다.

그래서 함수 원형만 따로 모아놓은 파일이 .h 확장자를 갖는 헤더파일(Header file)이고 이 파일만 #include로 불러오면 라이브러리의 함수를 모두 호출할 수 있게 되는 것입니다.


#include<stdio.h>

위 문장은 printf, scanf 등의 표준 입출력 함수들이 구현되어 있는 Standard Input / Output 라이브러리를 사용하기 위해 표준입출력 헤더파일을 포함한다는 의미입니다.


경우에 따라서는 함수의 선언과 동시에 구현을 하는 경우도 있습니다. 이러한 함수를 인라인 함수(Inline Function)이라고 합니다.

하지만 특별한 경우를 제외하고는 함수의 선언과 정의을 분리해서 사용하는게 좋습니다.


함수 정의

함수의 이름과 입/출력값을 지정하여 선언하였다면 이제 함수를 정의할 차례입니다. 다른 표현으로 함수를 구현한다고도 합니다.


DataType FunctionName(DataType, ...)

{

    // do something

    return value;

}


위와 같이 함수 원형 뒤에 { } 블럭에 실제 함수에서 수행할 코드를 작성해주면 됩니다.


함수 선언시에는 각 매개변수의 데이터 타입만 명시하고 이름을 생략할 수 있지만 함수 정의에서는 매개변수의 데이터 타입과 이름을 모두 명시해야 합니다.



// Declaration
int sum(int, int);

// Definition
int sum(int a, int b)
{
  return a + b;
}


위 예제는 두 개의 숫자를 매개변수로 입력받아서 두 숫자의 합을 반환하는 함수입니다.

이처럼 return 값은 특정 변수만 지정할 수 있는 것이 아니라 수식을 넣을 수도 있습니다. 그렇게 되면 수식식의 최종 결과값이 반환되게 되는 것이죠.


함수 호출

함수 정의까지 마쳤다면 이제 원하는 시점에 함수를 호출(Call) 할 수 있습니다.




아래와 같이 선언했던 함수명을 적고 ( ) 안에 매개변수에 들어갈 변수를 넣어주기만 하면 됩니다.

c = sum(a, b);

그러면 sum 함수로 프로그램이 점프(Jump)해서 실행을 하다가 return 을 만나면 다시 돌아오는 것이죠.
{ } 로 감싸여진 블록 마다 별도의 스택(Stack) 영역이 생긴다고 전에 언급 드린 적이 있었죠.

함수에서도 마찬가지입니다.
sum 함수가 호출되는 순간 sum 함수의 스택이 생성되고 스택 영역의 지역변수(Local variable)로 a와 b가 새롭게 할당됩니다.

그리고 main 스택에서 매개변수로 넣었던 a, b 변수의 값이 복사(Copy)되는데 이 부분이 중요합니다.

변수의 이름은 같지만 서로 다른 스택 영역에 존재하기 때문에 sum 함수에서 a, b 변수의 값을 아무리 변경하여도 main 스택 영역의 변수들의 값은 그대로입니다.

sum 함수의 } 부분에서 스택이 정리되면서 sum 함수 내에 할당되었던 변수 a, b는 소멸됩니다.

함수의 처리 결과를 main 스택에서 얻기 위해서는 반환값(return value)을 받는 수밖에 없습니다.

그런데 이 반환값는 한 개밖에 선언할 수가 없죠?

하지만 프로그램을 개발하다 보면 반환값이 여러 개가 필요한 경우가 종종 발생합니다. 이럴 경우에는 어떻게 해결해야 될까요?


첫 번째로는 구조체(struct)를 이용하는 방법이 있습니다.

반환값을 여러 개의 데이터 타입으로 구성된 구조체로 선언하면 원하는 만큼 다양한 결과값을 묶어서 반환할 수 있습니다.

구조체에 관련된 내용은 나중에 자세히 말씀드리기로 하고 오늘은 두 번째 방법을 소개해드리겠습니다.


바로 앞서 배웠던 포인터(pointer)를 이용해서 해결하는 방법입니다.


함수는 매개변수로 값을 그대로 넘기냐, 값에 대한 참조를 넘기냐에 따라서 다음과 같이 두 가지로 구분할 수 있습니다.

  • Call by value
  • Call by reference


위에 있는 sum 함수 예제는 전형적인 Call by value의 예입니다.


Call by reference를 구현하기 위해서는 매개변수포인터 변수로 선언해야 합니다.

sum 함수 예제를  Call by reference로 바꿔보겠습니다.



// Declaration
void sum(int, int, int*);

void main(void)
{
  int a,b,c;
  a = 10, b = 15, c = 0;
  sum(a, b, &c);
  printf("a + b = %d\n", c);
}

// Definition
void sum(int a, int b, int* c)
{
  *c = a + b;
}


int* 타입의 매개변수 c 를 선언하고 sum 함수를 호출할 때 변수 c주소를 넘깁니다.

그렇게 하면 매개변수 c에는 main 스택에 존재하는 변수 c 의 주소가 복사되겠죠.

결과적으로 sum 함수 실행 시 *c 와 같이 * 연산자를 이용해서 해당 주소의 실제 값을 변경하면 main 스택에 있는 변수 c 의 값이 변경되게 됩니다.

이러한 원리를 이용하면 매개변수의 개수에는 제한이 없기 때문에 원하는 만큼의 반환값을 함수로부터 받아올 수가 있습니다.



-Peter의 우아한 프로그래밍

여러분의 공감과 댓글은 저에게 크나큰 힘이 됩니다. 오류 및 의견 주시면 감사하겠습니다.

반응형