59 – 재귀호출의 예(피보나치 수열)

피보나치 수열은 처음 두 항의 값은 1이지만, 그 다음 항부터는 이전의 두 항의 합이 된다. 즉, 세 번째 항은 첫 항과 두 번째 항인 1을 더한 2가 되고, 네 번째 항은 두 번째 항과 세 번째 항을 합한 3이 된다. 이걸 그대로 쭉 적으면 다음과 같이 된다.

1   1   2   3   5   8   13   21   ……

즉, 처음 두 항을 제외한 나머지 항들에서 n항의 값은 n-1 항과 n-2 항을 더한 것이다. 피보나치 수열의 항에 따라 수식을 정리하면 다음과 같이 도니다.

  • fibo(n)
    • 1, n = 1
    • 1, n = 2
    • fibo(n-1) + fibo(n-2), n > 2

C 함수로 나타내면 다음과 같이 작성할 수 있을 것이다. 종료 조건이 n은 1이거나 2일 때 1을 반환하도록 작성하면 된다.

int fibo(int n)
{
if(n==1 || n==2)
return 1;
else
return fibo(n-1) + fibo(n-2);
}

그리고 이 함수를 이용해 하나의 예제로 만든 것이 아래의 예제 프로그램이다.

스크린샷 2017-04-05 오전 1.45.18

58 – 재귀호출의 예(1부터 n까지의 합)

어딜 뒤져봐도 제일 많이 나오는 예시일 것이다. 그래서 선택했다. 재귀 호출을 이용하여 1부터 n까지의 합을 구하는 함수를 작성해보자. 우선 sum(n)이라 하고, 다음과 같이 정의한다.

sum(n) = n + (n-1) + …… + 3 + 2 + 1

이걸 어떻게 재귀초 풀어야 하나를 생각해보면 바로 바로 생각은 안 날 것이다. 그래서 알고리즘 수업에서 이용하는 방법 중 하나인 작은 범위 안에서 무식하게 해보는 방법대로 해보자. 일단 5까지의 합을 생각해보자.

sum(5) = 5 + 4 + 3 + 2 + 1

이렇게 보면, 앞에 5를 제외한 나머지의 합의 경우에는 sum(4)와 같은 구조이다. sum(4)가 1+2+3+4이기 때문이다. 그걸 보면 다음과 같이 할 수 있다.

sum(5) = 5 + sum(4)

이 방법대로 쭉 내려가서 식을 작성해보면 다음과 같이 된다.

sum(5) = 5 + sum(4)
sum(5) = 5 + 4 + sum(3)
sum(5) = 5 + 4 + 3 + sum(2)
sum(5) = 5 + 4 + 3 + 2 + sum(1)
sum(5) = 5 + 4 + 3 + 2 + 1

이렇게 보면, sum(n)은 다음과 같은 구조라고 알 수 있다.

sum(n) = n + sum(n-1)

그러나, 이렇게만 하면 재귀가 종료가 되질 않는다. 음수 아래로 계속 끊기지 않고 연산할 수 있기 때문이다. 그렇기 때문에 이 재귀가 종료될 수 있는 조건을 제시해 줘야 한다. 여기에서는 1을 대입하게 되면 그대로 합이 1이 된다. 이걸 정의하면 1이 되었을 때 1을 할당하고 종료시킬 수 있게 된다. 이 내용을 하나의 내용으로 정리하면 아래와 같다.

  • sum(n)
    • 1, n=1
    • n + sum(n-1), n > 1

이를 C 함수로 나타내게 되면, n=1이면 1을 반환하고, 그렇지 않으면 n+sum(n-1)을 반환하도록 한다. 조건식으로 충분히 나눌 수 있고, 조건식 처리로 재귀 종료를 확인할 수 있는 것이다.

int sum(int n)
{
if(n==1)
return 1;
else
return n+sum(n-1);
}

이 함수를 이용한 예제가 아래의 예시이다.

57 – 재귀호출

알고리즘 공부하면서 제일 열받게 하는 것 중 하나이지만, 중요한 것이 나왔다.

C언어에서는 임의의 함수에서 자기 자신을 호출할 수 있는데, 이를 재귀 호출(recursion)이라고 한다. 이러한 재귀 호출을 사용하는 예가 다양하게 있는데, 그냥 간단한 예시 하나나 둘 정도 보면서 재귀 호출에 대해서 살펴보도록 하겠다.

일단 다음과 같은 구조를 보자.

void r_func();

main()
{
r_func();
}

void r_func()
{
r_func();
}

main 함수에서 함수 r_func를 호출하면 제어가 r_func로 넘어간다. 그런데 r_func에서 다시 r_func를 호출하는 과정이 계속 반복된다. 이와 같이 자신을 호출하는 것을 재귀 호출이라고 한다. 그런데 위와 같은 프로그램은 실행이 종료되지 않고 계속 반복되므로 재귀 호출 함수는 꼭 재귀 호출이 종료되는 조건을 설정해 주어야 한다. 그 설정을 만들어 주는 과정이 어렵기 때문에 동작 순서를 제대로 구성하지 않으면 안된다. 알고리즘에서 중요하게 다루는 이유가 이런 이유다.

56 – 함수 기억 클래스

함수의 기억 클래스는 외부(extern) 함수와 정적(static) 함수로 나뉜다. 외부 함수는 분리 컴파일 시 다른 파일에서 참조할 수 있으며, 기억 클래스 자리에 아무 내용이 없으면 외부 함수임을 의미한다. 반면에 정적 함수는 분리 컴파일 시 다른 파일에서 참조할 수 없으며, 기억 클래스 자리에 static을 작성하면 정적 함수임을 의미한다. 이 두 가지를 모두 보여주는 예로, 정의한 적정 함수를 호출은 하지만 사용이 불가능하다.

55 – 변수 기억 클래스(정적 변수)

자동 변수는 그 함수 내에서만 사용되며 함수에서 벗어나면 소멸하지만, 정적 변수(static variable)는 데이터 영역 이 기억 영역에 영구적으로 존재한다. 그리고 사용 용도에 따라 내부 정적 변수와 외부 정적 변수로 나뉜다.

  • 내부 정적 변수: 함수 안에서 선언된 정적 변수
  • 외부 정적 변수: 함수 밖에서 선언된 정적 변수

먼저, 내부 정적 변수는 함수 안에서 선언되며 그 함수 안에서만 사용할 수 있다. 그러나 자동 변수와 달리 그 함수를 빠져 나왔다가 다시 들어가도 이전 값이 그대로 유지된다. 그 예시가 아래와 같다.

그리고 외부 정적 변수는 함수 외부에서 정적으로 선언되는 변수로, 분리 컴파일 시 변수가 선언된 파일 내에서는 참조가 가능하지만 다른 파일에서는 참조할 수 없다. 다음은 이에 대한 예시로, 외부에서 선언한 정적 변수 count를 다른 파일에서 사용하려 하지만 사용이 불가능하다.

54 – 변수 기억 클래스(레지스터 변수)

일반적으로 변수는 메모리에 생성되지만, 레지스터 변수(register variable)는 메모리가 아닌 레지스터에 생성된다. 그런데 이러한 레지스터를 사용하면 메모리에서보다 더 빨리 처리할 수 있으므로 레지스터 변수는 반복문 제어 등 사용 빈도가 높은 변수에 이용하면 효율적이다. 그러나 다음과 같이 레지스터 변수를 선언해도 레지스터 수에는 한계가 있으므로 레지스터로 할당하지 못하는 경우에는 자동 변수로 처리가 된다는 점이 있다.

register int i;

53 – 변수 기억 클래스(외부 변수)

함수간의 변수를 공유하는 가장 대표적인 방법은 외부 변수를 사용하는 것이다. 외부 변수는 함수 밖에서 선언되고 어느 함수에서든 사용할 수 있으며, 생성된 외부 변수는 프로그램 실행이 종료될 때까지 메모리에서 사라지지 않는다.

아래의 예시 프로그램이 바로 외부 변수를 사용하는 예시로, 여기서는 외부 변수 count를 main 함수와 func 함수에서 공우하고 있다.

그리고 C언어에서는 하나의 프로그램을 여러 파일로 분리해 컴파일 할 수도 있는데, 이렇게 하면 큰 규모의 프로그램을 작성할 때 매우 유용하다. 그리고 이렇게 여러 파일로 분리해 프로그램을 작성할 때 외부 변수를 이용하면 여러 파일이 변수를 공유할 수 있다.

이번 예시 프로그램은 두 개의 파일로 구성된 프로그램이다. 외부 변수 count를 선언하고 외부 변수 count를 다른 파일에서 재선언한다. 이렇게 재선언하는 것은 어디에선가 선언된 외부 변수 count를 사용하겠다는 의미로 결국 다른 파일에서 선언한 외부 변수를 이용할 수 있게 되는 것이다. 그때 사용한 것이 extern이다.

51 – 기억 클래스

C언어에서 변수와 함수는 데이터형과 기억 클래스라는 두 속성을 지니는데, 이 중 메모리의 어느 위치를 어떻게 확보하는지를 결정하는 것을 ‘기억 클래스’라고 한다. 변수와 함수가 참조될 수 있는 영역에 따라 다음의 4가지로 나뉘어 볼 수 있다.

  • auto
  • extern
  • register
  • static

그 종류에 따라 변수와 함수가 참조될 수 있는 영역이 결정되는 기억 클래스는 다음과 같은 형태로 사용한다.

[기억클래스] [데이터형] [변수(혹은 함수)이름]

예를 들어, static 클래스의 int 형 변수 i의 선언을 한다면 다음과 같이 한다.

static int i;

이제 변수와 함수의 기억 클래스에 대해 각각 확인해야한다. 내용이 많아서 글을 여럿 나눌 것이다.

50 – 함수간 데이터 전달 기법

함수가 제대로 동작하려면 한 함수에서 다른 함수를 호출할 때 필요한 값을 전달해주기도 하고 결과값을 전달받기도 해야 하는데, 함수들끼리 주고받는 데이터를 매개변수(parameter)라고 한다. 그리고 특별히 함수를 호출하는 쪽의 매개변수를 실매개변수(actual parameter)라고 하고, 함수를 정의한 쪽의 매개변수를 형식매개변수(formal parameter)라고 한다.

C언어에서 함수를 호출할 때 매개변수를 전달하는 방법에는 다음 두 가지가 있다. 하나하나 살펴본다.

우선 ‘값에 의한 전달(call by value)’는 함수 호출 시 값만 함수쪽으로 보내 해당 값을 매개변수에 저장해 동작하는 방법으로, 함수 실행중에 형식매개변수의 내용이 변해도 실매개변수의 니용은 전혀 변하지 않는다. 즉, 값이 그대로 함수쪽의 매개변수에 복사되어 별도의 변수처럼 취급되는 것이다.

그리고 ‘주소에 의한 전달(call by address)’ (call by reference라고도 합니다.)은 값을 전달하는 것이 아닌 전달하고자 하는 변수의 메모리 주소를 전달하는 방법으로, 함수 실행중에 형식 매개변수 값의 조정에 따라 실매개변수의 내용이 변경되는 방법이다. 당연히 포인터에 대한 개념이 제대로 있어야 쓸 수 있다.

이 두 가지를 직접 하나의 예제에 박아서 작성하였다. 함수명에도 적혀있지만 call by value, call by address(call by reference) 형식으로 전달하는 방법을 보여주는데, 결과값이 확실히 다른 것을 볼 수 있다.

call by value로 호출한 함수의 경우에는 두 변수의 값을 바꾸는 작업을 하였는데도 불구하고 함수에서 쓰인 변수와 main에 선언한 변수는 별개의 값으로 취급되기 때문에 둘의 값을 바꾼 건 함수 안에서만의 동작이고 실제로는 바뀌지 않았다. 그러나 call by address의 경우에는 main에서 선언한 변수를 포인터로 가져와서 치완하는 작업을 진행하였기 때문에 해당하는 값을 그대로 바꿨다. 그래서 출력 결과가 바뀌어 있는 것이다.