8-2.난수 함수

8-2-가.표준 난수 함수

이번에는 일반적인 함수들과는 조금 다른 특이한 난수 함수에 대해 알아보자. 난수(Random Number)란 무작위로 만들어지는 알 수 없는 값이다. 마치 주사위를 던졌을 때 어떤 수가 나올지 미리 알수 없는 것처럼 말이다. 어떤 값을 가지게 될 지 예측할 수 없는 수라는 뜻인데 이런 난수가 필요한 이유는 말 그대로 예측을 허용하지 않기 위해서이다.

만약 포커 게임을 만드는데 게임을 할 때마다 나오는 패가 동일하다면 이 게임은 정말 재미없을 것이다. 또한 슈팅 게임에서 적이 움직이는 경로에 일정한 규칙이 있다거나 퍼즐 게임의 퍼즐이 예측 가능하다면 이 또한 제대로 된 게임이라고 할 수 없다. 패를 무작위로 섞기 위해, 적의 움직임을 미리 알 수 없도록 하기 위해 난수가 필요하다.

난수를 만들 때는 일반적으로 random 이라는 함수를 사용하며 난수 루틴을 초기화할 때는 randomize라는 함수를 사용한다. 그러나 이 함수들은 진짜 함수가 아니라 매크로로 정의되어 있는 가짜 함수들이다. 가짜 함수만 쓸 수 있어도 난수를 만드는데는 큰 불편함이 없지만 내부를 좀 더 정확하게 이해하기 위해 이 매크로 함수들을 분석해 보자. 난수를 만드는 진짜 함수는 다음 두 개이다.

 

int rand(void);

void srand(unsigned int seed);

 

rand 함수는 0~RAND_MAX 범위의 수 중에서 무작위로 한 수를 생성해 낸다. RAND_MAX는 컴파일러에 따라 다르지만 일반적으로 32767(0x7ffff)로 정의되어 있다. 그래서 rand 함수를 호출하면 0부터 32767중의 임의의 정수 하나가 리턴된다. 이 함수를 테스트하는 간단한 예제를 만들어 보자.

 

: random

#include <Turboc.h>

 

void main(void)

{

     int i;

     for (i=0;i<10;i++) {

          printf("%d ",rand());

     }

}

 

rand 함수를 10번 호출하여 이 함수가 만들어 내는 난수를 화면으로 출력해 보았다. 실행 결과는 다음과 같다.

 

41 18467 6334 26500 19169 15724 11478 29358 26962 24464

 

보다시피 일정한 규칙이 없는 10개의 난수를 마구 생성했다. 그러나 이 rand 함수만으로 무작위 난수를 만들기에는 부족한 면이 있는데 이 예제를 여러 번 실행시켜 보도록 하자. 난수가 무작위로 나오기는 하는데 그 순서가 실행할 때마다 똑같다. rand 함수는 일정한 규칙에 따라 난수를 생성하는데 규칙이 항상 같기 때문에 난수가 생성되는 순서도 항상 같을 수밖에 없다. 이래서는 제대로 된 난수라고 할 수가 없다.

그래서 난수 생성 루틴의 규칙에 변화를 줄 수 있는 srand라는 함수가 필요하다. srand는 난수 발생기에 난수를 발생시키는 시작점(seed)를 제공하며 난수 발생기는 이 시작점을 기준으로 하여 난수를 발생시킨다. 따라서 시작점을 바꾸면 생성되는 난수도 달라진다. int i; 다음에 srand(1234)나 srand(5678) 같은 호출문을 삽입하면 발생되는 난수가 달라질 것이다.

그러나 이렇게 하더라도 시작점이 동일하면 생성되는 난수에는 일정한 규칙이 존재할 수밖에 없다. 완전한 난수를 만들기 위해서는 난수 생성기에게 전달되는 시작점 또한 예측 불가능한 난수여야 하는 것이다. 정확한 계산을 하는 기계인 컴퓨터가 완전히 예측 불가능한 난수를 만들어 내는 것은 무척 어려운 일이다. 난수 생성기에게 줄 시작점이 난수여야 한다니 참 곤란하다.

하지만 다행스럽게도 컴퓨터에는 난수 발생기의 시작점으로 쓸 수 있는 진짜 난수가 하나 있는데 바로 시간이다. 난수 발생기가 실행될 시점의 시간은 예측할 수 없기 때문에 시간값을 시작점으로 사용한다면 완전한 난수를 만들 수 있다. 위 예제의 int i; 뒤에 다음 코드를 추가해 보자.

 

srand((unsigned)time(NULL));

 

time 함수는 현재 시간을 나타내는 정수값을 리턴하는데 이 값을 시작점으로 사용하면 프로그램이 실행될 때마다 완전히 다른 난수를 만들어낼 수 있다. time 함수의 대용으로 GetTickCount라는 API함수를 사용할 수 있는데 이 함수는 시스템이 부팅된 후 경과된 시간을 리턴하므로 시작점으로 쓰기에 적합하지만 윈도우즈 API 함수를 사용하면 이식성이 떨어지는 단점이 있다. clock 이라는 함수도 시작점으로 쓸 수 있다.

난수 발생기를 초기화하는 문제는 해결되었다. 다음은 rand 함수를 좀 더 쓰기 좋도록 개량해 보자. 이 함수는 0~32767 사이의 임의 정수값 하나를 구하는데 난수는 보통 일정한 범위에 속해 있어야 의미가 있다. 예를 들어 고스톱 패라면 0~47 사이에 있어야 할 것이고 주사위의 눈은 0~5 사이에 있어야 한다. rand 함수가 리턴하는 난수를 일정한 범위안의 수로 바꿀 때는 나머지 연산자 %를 사용한다.

 

rand() % 48       // 0~47 사이의 난수

rand() % 6         // 0~5 사이의 난수

 

어떤 임의의 수를 a로 나눈 나머지는 항상 a보다 작다. 나머지 연산자를 적절한 위치에 제대로 사용한 예라고 할 수 있다. srand 함수를 사용하면 난수 발생기를 초기화할 수 있고 rand 함수와 나머지 연산자를 사용하면 원하는 범위의 난수를 만들 수 있다. 그러나 이 함수들은 쓰기에 불편하기 때문에 직접 사용하지 않으며 다음과 같이 정의되어 있는 매크로 함수를 대신 사용한다.

 

#define randomize() srand((unsigned)time(NULL))

#define random(n) (rand() % (n))

 

randomize 함수는 현재 시간을 사용하여 난수 발생기를 초기화하며 random 함수는 인수로 전달된 n사이의 난수를 발생시킨다. rand, srand 함수보다 훨씬 더 직관적이고 원형이 간단하기 때문에 아주 옛날부터 난수 생성을 위해 이 두 매크로 함수를 사용하는 것이 정석이었다. 그래서 볼랜드사의 터보 C, 볼랜드 C 계열 컴파일러는 이 두 함수를 stdlib.h 헤더 파일에 정의해 놓았다.

그러나 볼랜드 이외의 컴파일러들은 이 매크로를 헤더 파일에 정의해 놓지 않아서 rand, srand 함수를 직접 사용해야 하는 불편함이 있다. 하지만 컴파일러가 제공하지 않는다고 randomize, random 함수를 못 쓰는 것은 아니며 필요하면 이 매크로를 직접 정의해서 사용할 수 있다. 그래서 이 책은 Turboc.h 헤더 파일에 이 두 매크로를 미리 정의해 놓았으며 이 헤더만 포함하면 터보 C를 쓰듯이 두 함수를 자유롭게 사용할 수 있다. 만약 실전에서 이 두 함수가 필요하면 Turboc.h에 있는 매크로 정의문을 복사해서 사용하면 된다.

비주얼 C++로 실습을 진행하다 보니 불필요한 설명이 좀 길어진 것 같다. 요약하자면 난수 발생기를 초기화할 때는 randomize 함수를 사용하고 0~n사이의 난수를 생성할 때는 random(n)을 호출하면 된다.