혼자 공부하는 C - 함수




함수를 만들기 위한 3가지

  • 함수 정의: 실제 코드 구현
  • 함수 호출: 함수 사용
  • 함수 선언: 사용할 함수에 대해 컴파일러에 정보를 주기



함수 정의

함수를 만들 때에는 이름, 필요한 데이터, 수행의 결과를 생각해야합니다. 각각의 결과가 함수의 prototype이 되고, 이를 토대로 함수를 정의하게 됩니다.

구조는 아래와 같습니다.


1
반환명 함수명(매개변수);



예시


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>

int sum(int x, int y); // 함수 선언

int main(void)
{
printf("%d\n", sum(1, 2)); // 함수 호출
}

int sum(int x, int y) // 함수 정의
{
return (x + y);
}

// 3

참고로, 함수 내에서 변수를 정의가 가능하며 Local variable로 지정되므로, 다른 함수에서의 변수와 이름을 같게 하여도 독립적으로 지정됩니다.



함수 선언

함수 정의를 하는데 함수 선언이 필요한 이유는 무엇일까요?

컴파일러가 함수를 인식할 수 있도록 알리는 역할을 합니다.


필요한 이유는 다음과 같습니다.

  1. 함수 선언에서 반환값의 형태를 확인합니다.
    • 함수를 선언하면 반환값을 알 수 있고 컴파일러 호출 전에 미리 알릴 수 있습니다.
    • 함수 호출 전에 함수 정의를 하였다면, 선언을 하지 않아도 괜찮습니다.
    • 하지만, 코드가 길어지고 여러 함수가 존재하면 함수 간에 호출관계가 엉킬 수 있고, 순서에 맞게 정의하는게 쉽지 않으므로, 함수 선언은 main 전에 모두 하고, 함수 정의는 main 하위에 작성하는 것이 좋습니다.
  2. 함수 호출 형식에 문제가 없는지 검사합니다.
    • 함수 선언에는 반환 값과 파라미터의 data type이 정의되어 있으므로, 함수 사용시 데이터를 알맞게 주었는지 미리 확인이 가능합니다.



매개변수가 없는 함수

매개 변수가 없는 경우는 매개변수에 void를 입력하여 없음을 표시합니다.


1
반환명 함수명(void);

void가 없어도 매개변수가 없다는 사실을 표시할 수 있으나, void를 입력하여 명시적으로 표현하는 것이 좋습니다.



반환값이 없는 함수

함수는 기능에 따라 형태가 결정되며, 반환값이 필요 없는 함수도 있습니다
예) 화면에 print만 하는 함수 등

이 경우 반환값에 void를 입력하여 반환값이 없음을 표시합니다. 또한 함수 정의에서 return에 아무것도 없이 작성합니다.


1
2
3
4
5
6
7
void 함수명(매개변수); // 선언

void 함수명(매개변수) // 정의
{
// do something
return;
}



재귀호출 함수

재귀 호출 (recursive call) 함수는 자기 자신을 호출하는 함수를 의미합니다.

아래 코드는 apple을 출력하는 fruit함수를 재귀적으로 호출합니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>

void fruit(void);

int main(void)
{
fruit();
return 0;
}

void fruit(void)
{
printf("apple\n");
fruit();
}



결과는 수많은 apple을 stdout으로 출력하고 Segmentation fault (core dumped)를 stderr로 출력합니다.


왜 중단되었을까?

재귀함수를 call하는 stack에는 제한이 있습니다.
함수의 call마다 stack 영역에 정보를 저장하는데, 제한을 넘기면 중단이되며 이 상황을 stack overflow라고 합니다.
이 코드에서도 stack 영역을 모두 사용하였으므로, Segmentation fault가 발생합니다.

다음 링크를 참고


이러한 중단은 정상적인 상황이 아니므로, 재귀함수에서는 탈출할 수 있는 조건식을 항상 염두하여 작성하여야 합니다.




번외

재귀 함수를 이용한 피보나치 수열 구현해보기

피보나치 수열은 앞 2개의 값을 더한 값이 현재 원소의 값이 되는 수열입니다.

0 1 1 2 3 5 8 13 21 …


1. 재귀 함수를 이용한 구현


1
2
3
4
5
6
7
8
9
10
11
12
13
int fibo(int n)
{
switch(n)
{
case 0:
return 0;
case 1:
return 1;
case 2:
return 1;
}
return fibo(n-1) + fibo(n-2);
}

주어진 값에 대해 하위 n-1과 n-2의 값을 재귀함수로 호출하는 함수입니다.


이 코드의 단점은 중복된 계산이 존재하는데,

n을 5라고 하면 3은 두번 실행됩니다.

Tree의 2아래는 안적었지만, fibo(2)도 fibo(0) + fibo(1) 이므로 Tree의 높이는 항상 n과 같으며, 이 재귀함수의 속도는 $ O(2^n) $이 됩니다.

따라서, 결과를 미리 저장하는 메모이제이션을 추가하면 다음처럼 구현할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int memo[100] // 초깃값이 모두 0인 array 
int fibo_memo(int n)
{
switch(n)
{
case 0:
return 0;
case 1:
return 1;
case 2:
return 1;
}

if (memo[n] != 0)
return memo[n];
else
{
memo[n] = fibo_memo(n-1) + fibo_memo(n-2);
}

return memo[n];
}

값을 저장하면서 있으면 바로 return을 하기 때문에 시간은 O(n)이 됩니다.

하면서 알게 된 점

  • C에는 default parameter가 없다.
  • dictionary나 Hashmap은 직접 구현해야한다.



2. static 변수를 사용한 재귀함수


1
2
3
4
5
6
7
8
9
10
11
12
13
14
int fibo_static(int n)
{
static int first = 0;
static int second = 1;
static int current = 0;

if (n == 0)
return current;

current = first + second;
first = second;
second = current;
return fibo_static(n-1);
}

하면서 알게된 점

  • static 변수

static 변수는 추후 393 페이지에서 언급됩니다.

static 변수는 프로그램이 끝날 때 까지 메모리에서 사라지지 않으며, 한번 초기화 하면 재초기화가 되지 않습니다.

따라서, 함수 내에 초기화하는 Line이 있음에도 초기화 되지 않고, 직접 지정한 값으로 이어져 재귀적으로 실행이 됩니다.



오늘은 C언어의 함수에 대해서 정리해보았습니다.

읽어주셔서 감사합니다👋👋