. CHistory

MRU에 저장되는 대상은 파일의 경로이며 파일의 경로는 문자열이므로 먼저 문자열 히스토리 클래스를 Util.h에 다음과 같이 선언한다.

 

class CHistory

{

private:

     TCHAR **data;

     int width;

 

public:

     int num;

     int height;

 

     CHistory();

     CHistory(int awidth,int aheight);

     ~CHistory();

 

     void Init(int awidth, int aheight);

     void ChangeHeight(int aheight);

     void Add(TCHAR *str);

     void Delete(int idx);

     TCHAR *Get(int idx);

     void Set(int idx, TCHAR *str);

     void Empty();

};

 

히스토리 클래스는 동적으로 관리되는 문자열 배열이라고 생각하면 된다. data는 문자배열의 배열, 즉 문자열 포인터 배열이다. 배열이 동적으로 관리되므로 미리 정해진 크기의 버퍼를 가지지 않으며 실행중에 필요한 만큼 메모리를 할당할 것이다. width는 한 문자열의 폭이며 height는 문자열의 최대 개수이다. 히스토리에 저장되는 문자열의 최대 길이는 width이며 이런 문자열을 최대 height 개수만큼 가질 수 있다.

문자열의 개수는 실행중에 자유롭게 변경할 수 있으나 폭은 일단 정해지면 변경할 수 없다. 문자열의 폭을 늘리는 것은 어려운 일이 아니지만 줄일 때는 이미 들어 있는 데이터를 처리하기가 골치 아파지기 때문이다. 또한 히스토리는 그 특성상 개수는 가변적이더라도 저장되는 정보의 포맷이 가변적이지는 않기 때문에 이런 규칙이 별 문제가 없다. 파일의 경로는 MAX_PATH의 길이를 가지는 문자배열 포맷으로 고정되어 있다. num은 현재 등록된 문자열의 개수이다. 이 클래스의 구현 코드를 Util.cpp에 다음과 같이 작성한다.

 

CHistory::CHistory()

{

}

 

CHistory::CHistory(int awidth, int aheight)

{

     Init(awidth,aheight);

}

 

CHistory::~CHistory()

{

     int i;

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

          free(data[i]);

     }

     free(data);

}

 

void CHistory::Init(int awidth, int aheight)

{

     int i;

 

     width=awidth;

     height=aheight;

 

     data=(TCHAR **)malloc(sizeof(TCHAR *)*height);

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

          data[i]=(TCHAR *)malloc(width);

          memset(data[i],0,width);

     }

     num=0;

}

 

void CHistory::ChangeHeight(int aheight)

{

     int i;

 

     if (aheight > height) {

          for (i=height;i<aheight;i++) {

              data[i]=(TCHAR *)malloc(width);

              memset(data[i],0,width);

          }

     }

 

     if (aheight < height) {

          for (i=aheight;i<height;i++) {

              free(data[i]);

          }

     }

 

     height=aheight;

     num=min(num,height);

}

 

void CHistory::Add(TCHAR *str)

{

     int i,nFind=10000;

 

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

          if (lstrcmp(str,data[i])==0) {

              nFind=i;

              break;

          }

     }

 

     for (i=min(nFind,min(num,height-1));i>0;i--) {

          lstrcpy(data[i],data[i-1]);

     }

     lstrcpy(data[0],str);

     if (nFind == 10000 && num < height) {

          num++;

     }

}

 

void CHistory::Delete(int idx)

{

     int i;

     for (i=idx;i<height-1;i++) {

          lstrcpy(data[i],data[i+1]);

     }

     lstrcpy(data[height-1],"");

     num--;

}

 

TCHAR *CHistory::Get(int idx)

{

     return data[idx];

}

 

void CHistory::Set(int idx, TCHAR *str)

{

     lstrcpy(data[idx],str);

}

 

void CHistory::Empty()

{

     int i;

 

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

          lstrcpy(data[i],"");

     }

}

 

두 개의 생성자가 정의되어 있지만 실제로 클래스를 초기화하는 함수는 Init이다. Init는 문자열의 폭과 최대 개수(=높이)를 인수로 전달받아 이 문자열을 기억하기 위한 포인터 배열을 할당한다. 모든 문자열은 NULL로 초기화되며 등록된 문자열 개수 num 0이 된다. Init는 필요한 만큼 메모리를 할당하고 문자열 버퍼를 완전히 초기화한다. 이렇게 할당된 버퍼는 파괴자에서 해제된다. CHistory는 특별히 금지를 하고 있지 않지만 Init는 반드시 한 번만 호출해야 한다. 이미 할당한 상태에서 이전 data를 삭제하지 않고 또 할당하면 메모리 누수가 발생한다.

히스토리 객체는 MRU뿐만 아니라 검색 문자열, 검색 폴더 등의 문자열 정보를 관리하는 용도로도 사용된다. 이런 정보들은 단순한 문자열 배열과는 다른 특징을 가지고 있으며 그래서 CHistory도 이런 정보 저장에 적합한 두 가지 중요한 특징을 가진다.

첫 번째 특징은 무한 배열이 아니라 유한 배열이라는 점이다. 히스토리 정보는 무한정 쌓이기만 하는 것이 아니라 일정한 개수에 이르면 오래된 정보는 삭제되어야 한다. 히스토리를 유지하는 목적이 자주 사용하는 정보를 빨리 선택하도록 하자는 것인데 정보의 개수가 무한하다면 그 가치가 떨어지게 된다. 그래서 저장할 정보의 최대 개수를 height 멤버에 기억하여 이 개수를 넘어서면 제일 오래된 정보를 삭제한다.

비록 유한 배열이기는 하지만 개수 자체는 실행중에도 변경할 수 있도록 되어 있으며 ChangeHeight 함수가 이 작업을 한다. 변경된 개수가 이전 개수보다 더 많다면 추가된 만큼 새로 메모리를 할당하고 더 작다면 불필요해진 메모리를 반납하면 된다. CHistory가 실행중에 배열의 크기를 조정할 수 있으므로 Dangeun은 재시작없이도 MRU의 개수를 마음대로 변경할 수 있다.

CHistory의 두 번째 특징은 항목을 추가할 때 무조건 배열의 처음에 삽입하지 않고 이미 등록되어 있는 항목일 경우 제일 위로 이동시켜 준다는 점이다. 그래서 같은 정보를 중복 삽입하지 않을 뿐만 아니라 가장 최근에 검색된 문자열이 항상 목록의 제일 처음에 배치된다. 중복 방지 및 이미 있는 항목에 대한 처리는 항목을 삽입하는 Add 함수에 작성되어 있다.

이 함수가 어떻게 항목을 추가하는지 구체적인 예를 통해 살펴보도록 하자. 새로 추가된 문자열은 목록의 제일 위인 data[0]에 저장하고 나머지는 한 칸씩 아래로 복사하여 내린다. 아래쪽으로의 이동이므로 복사 방향은 목록의 끝에서부터 위로 올라와야 한다. 세 개의 목록이 있는 상태에서 하나가 더 추가될 때의 동작은 다음과 같다.

A,B,C 세 항목은 한 칸씩 아래로 복사하여 내린다. 이때 A B, B C, C를 빈칸으로 복사하는 것이 아니라 C를 빈칸으로, B C, A B로 복사해야 한다는 점을 주의해야 한다. 원래 A가 있던 data[0] 위치에 D를 삽입함으로써 새로 삽입된 항목이 제일 위쪽에 배치되도록 하였다.

이미 목록에 있는 문자열이 추가될 때는 새로 추가되는 문자열을 목록의 제일 위로 올리고 그 앞쪽에 있던 항목들만 한 칸씩 아래로 내린다. , 새로 항목을 추가하지는 않고 목록이 한 바퀴 회전하기만 한다. 새로 입력된 문자열이 목록에 있는지 찾아보고 있으면 nFind에 그 인덱스를 대입한다. 목록에 없으면 nFind의 초기값은 10000이 될 것이다. 예를 들어 여섯 개의 목록이 있는 상태에서 D가 다시 추가될 때의 동작은 다음과 같다.

D가 제일 위로 올라가고 A,B,C는 각각 한 칸씩 아래로 이동한다. E,F는 변화가 없으며 그 자리에 가만히 있으면 된다. 복사를 시작할 위치는 일단 항목 개수인 num이되 height-1보다 클 수는 없다. 왜냐하면 최대 개수가 이미 등록되어 있으면 제일 아래쪽의 마지막 항목은 삭제되어야 하기 때문이다. 또 목록에 이미 항목이 있으면 그 인덱스인 nFind가 복사 시작위치가 된다. i=min(nFind,min(num,height-1)) 식은 세 값 중 가장 작은 값을 선택한다는 뜻이며 nFind의 초기값 10000은 목록에서 발견되지 않을 경우를 위한 충분히 큰 값이다.

새로운 항목을 추가한 후 등록된 항목 개수인 num 1 증가시킨다. , 이미 있는 항목이 위치만 옮겼다거나 최대 개수만큼 이미 등록되어 있는 경우는 num값에 변화가 없다. Add 함수는 길이가 짧지만 보기보다 복잡한 처리를 하고 있으며 CHistory 클래스의 특징을 결정짓는 핵심 코드를 담고 있다.

나머지 함수들은 읽으면 바로 이해가 될 정도로 코드가 쉽다. Delete 함수는 항목을 삭제하는데 삭제된 항목 이후의 항목을 앞쪽으로 복사한다. Get 함수는 히스토리 문자열을 읽어주는데 목록의 첫 번째 항목을 읽고 싶으면 Get(0)를 호출하면 된다. Set 함수는 지정한 인덱스의 문자열을 직접 변경한다. Empty 함수는 모든 문자열을 비운다.