C++Builder Programming Forum
C++Builder  |  Delphi  |  FireMonkey  |  C/C++  |  Free Pascal  |  Firebird
볼랜드포럼 BorlandForum
 경고! 게시물 작성자의 사전 허락없는 메일주소 추출행위 절대 금지
C++빌더 포럼
Q & A
FAQ
팁&트릭
강좌/문서
자료실
컴포넌트/라이브러리
메신저 프로젝트
볼랜드포럼 홈
헤드라인 뉴스
IT 뉴스
공지사항
자유게시판
해피 브레이크
공동 프로젝트
구인/구직
회원 장터
건의사항
운영진 게시판
회원 메뉴
북마크
볼랜드포럼 광고 모집

C++빌더 Q&A
C++Builder Programming Q&A
[2038] Re:[질문] TList로 파일리스트 만들어 출력하는데 문제가?
박지훈.임프 [cbuilder] 3788 읽음    1999-10-02 00:00
: 안녕하세요.
: 이곳에 답변이 빨라.. 자주 이용하는데 꼭좀 부탁드립니다.
: ^^;
:
: 도스에 dir처럼 파일리스트 출력하고 싶어서.. 아래처럼했는데
: 출력은 되는데 꼭 파일하나씩 부족하게 출력되거든요.
: 어디에 문제가 있는지 좀 봐주세요.
:
: typedef struct _FLIST
: {
:     char FileName[MAXPATH];
: }FLIST;
:
: TList *FileList;
:
: void __fastcall TFrmMain::FormCreate(TObject *Sender)
: {
:     FileList = new TList;
:     if(FileList == NULL)  return;
:
:     MakeFileList("c:\\*.*");
: }
:
: void __fastcall TFrmMain::FormClose(TObject *Sender, TCloseAction &Action)
: {
:     delete FileList;
: }
:
: void __fastcall TFrmMain::MakeFileList(char *dirname)
: {
:     FLIST *p;
:     struct ffblk file;
:     int done;
:
:     if(FileList->Count > 0) FileList->Clear();
:
:     done = findfirst(dirname, &file, 0xF7);
:     while(!done)
:     {
:         p = new FLIST;
:         if(p == NULL) return;
:
:         strcpy(p->FileName, file.ff_name);
:
:         FileList->Add(p);  //파일리스트 추가
:         done = findnext(&file);
:     }
:
:     for(int i=0; i<FileList->Count; i++)  delete p;
: }
:
: 출력부분은 맞는것 같아서.. 않올리구요..
: 위에 리스트를 만드는 부분에 문제가 있는것 같은데 어떻게 해야 되는지 꼭 좀
: 가르쳐 주심 감사하겠습니다.



임펠리테리입니다.

사소한 실수를 하셨습니다. TList는 언뜻 보기보다 사용이 그리 간단하지 않은 클래스입니다.
TList는 아이템으로 넣어진 포인터들의 복사본을 가지고 있을뿐,
그 메모리 영역을 직접
관리해주지 않습니다. 파일을 모두 찾아서 FileList 객체에 넣어준 후 다음 코드를 추가하셨는데,
for(int i=0; i<FileList->Count; i++)  delete p;
이 한 라인 때문에 문제가 생기는 것입니다. 아마도 할당해준 메모리들을 TList 객체가
관리해줄 것으로 생각하시고 임시로 사용한 p 포인터를 삭제하려고 시도하셨는데, 여기서 p는
임시로 사용한 포인터일 뿐이므로 매번 루프를 돌 때마다 다시 값이 바뀌었으므로
for 문을 돌리면서 delete 한다고 해도 마지막에 메모리를 할당한 영역만 파일 갯수만큼
해제하려고 시도하게 됩니다. 게다가 앞에서 말했듯이 TList는 메모리를 직접 관리해주지
않으므로, TList에 마지막으로 집어넣은 아이템은 무효하게 되는 것입니다.

자세히 따지자면... 한 줄의 코드로 두번의 실수를 하시는 성과(^^;;)를 거두셨습니다.
무엇이 문제인지를 더 확실히 짚고 넘어가기 위해, 이 한 라인이 실제로 어떻게 문제가
되는지 생각해봅시다. 가장 치명적인 것은(아마도 이 버그는 아직 나타나지 않은듯합니다만)
파일 갯수 - 1만큼 잘못 메모리를 해제한 데 따른 억세스 바이얼레이션의 발생 위험입니다.
만약 파일이 10라고 하면, 앞에서 말했듯이 p는 마지막 파일이름만을 가리키고 있을 뿐이므로
이 동일한 p 포인터를 10번 해제하려고 시도한 것입니다. 그러면 첫번째 시도를 제외하면
9번을 이미 해제된 메모리에 대해 해제시도를 한 셈인데, 이미 메모리를 해제한 영역을
다시 해제하려고 시도하면 최악의 경우 억세스 바이얼레이션이 발생합니다.
두번째 문제는 발견하신 문제, 즉 리스트의 마지막 아이템이 메모리가 할당되지 않은 엉뚱한
영역을 가리키고 있다는 것입니다. for 루프의 마지막 실행에서 분명 마지막으로 발견된
파일이름을 TList에 집어넣어주었지만, 이 아이템의 포인터는 역시 p도 여전히 가리키고 있는데,
이것을 마지막 delete 문을 실행함으로써 해제해버려 이 영역에는 실제로 파일 이름이
들어있지 않게 된겁니다. 그래서 결과적으로 다른 파일들은 모두 나오는데, 마지막으로
발견된 파일만 나타나지 않는 것처럼 보이게 됩니다.

한가지 더 확실히 할 것은.. TList 에 추가한 아이템들을 마지막에 해제하려고 하신 것으로
보아, 아마도 따로이 이 영역을 해제하는 코드는 작성하시지 않으신 것 같습니다.
TList에 포함된 각 아이템들은 그 정보가 필요없어질때까지 삭제하면 안되며, 또 한편으로는
TList::Delete() 메소드를 호출한다고 해서 내부에 가지고 있는 포인터가 가리키는
메모리 영역까지 해제해주지도 않습니다. 그러므로 파일이름들의 리스트가 필요없어지는 시점
(물론 새로이 파일리스트를 만드는 시점을 포함해서)에서 List의 각 아이템에서 Delete()
(혹은 Clear())메소드를 실행하기 직전에 각 아이템에 할당된 메모리를 직접 해제해주어야
합니다. 그런데, 여기서 또한가지 골치아픈 문제가 있는데, TList 항목에 저장되는
포인터는 void *형으로 캐스팅되어서 저장되어 있으므로, 단순히 delete를 호출한다고 해서
올바르게 해제되지 않습니다. 그러므로 반드시 원래 할당했던 타입으로 캐스팅을 해주어야 합니다.
(이것은 TList의 아이템을 사용하기 위해서는 반드시 원래의 형으로 캐스팅을 해야 하는 것과
마찬가지입니다.)

간단히 몇줄로 문제점만 지적하고 그만둘 수도 있겠습니다만, 사실 TList는 사용하기에
결코 만만하게 볼 클래스가 아니란 점을 강조하고자 일일이 문제점을 들어 설명을 드렸습니다.
너무 세세하게 설명을 해서.. TList에 겁을 먹으실 지도 모르겠는데, 실제 수정한 예를
보시면 그렇게 아주 복잡한 것은 아니란 것을 알게 되실 겁니다. 하지만 앞에서 말씀드린 사항들은
TList를 사용하면서 항상 고려하고 있어야 하는 점들입니다. 그럼 정정한 소스를 봅시다.

void __fastcall TFrmMain::FormCreate(TObject *Sender)
{
    FileList = new TList;
    if(FileList == NULL)  return;

    MakeFileList("c:\\*.*");
}

void __fastcall TFrmMain::FormDestroy(TObject *Sender)
{
    ClearFileList();
    delete FileList;
}

void __fastcall TFrmMain::ClearFileList(void)
{
    while(FileList->Count != 0)
    {
        delete (FLIST *)FileList->Items[FileList->Count-1];
        FileList->Delete(FileList->Count-1);
    }
}

void __fastcall TFrmMain::MakeFileList(char *dirname)
{
    FLIST *p;
    struct ffblk file;
    int done;

    ClearFileList();
    done = findfirst(dirname, &file, 0xF7);
    while(!done)
    {
        p = new FLIST;
        if(p == NULL) return;
        strcpy(p->FileName, file.ff_name);

        FileList->Add(p);
        done = findnext(&file);
    }
}

위의 소스에서 몇가지 더 참고하실 만한 사항을 알려드립니다.
FileList 객체를 생성한 것이 폼의 OnCreate이므로 해제 또한 그에 해당하는 OnDestroy
핸들러에서 하도록 수정했습니다. 이처럼 반드시 메모리의 할당과 해제는 쌍으로 존재하도록
코딩하는 버릇을 들여두시는 것이 좋습니다.
또한가지, 리스트의 아이템을 모두 삭제하는 ClearFileList() 함수를 눈여겨 보시기 바랍니다.
앞에서 말했듯이, 먼저 각 아이템의 메모리를 해제한 후에 아이템의 Delete() 메소드를
호출했다는 것은 이해하실테구요. 해제하는 인덱스의 순서를 잘 보시기 바랍니다. 만약
쉽게 생각할 수 있는대로 다음과 같이 코딩한다면,
void __fastcall TFrmMain::ClearFileList(void)
{
    for(int i=0; i<FileList->Count; i++)
    {
        delete (FLIST *)FileList->Items[i];
        FileList->Delete(i);
    }
}
이런 코딩은 문제가 됩니다. 제대로 모든 아이템이 삭제되지 않습니다. TList는, 중간의
한 아이템이 삭제되면 그 뒤의 아이템들의 인덱스는 자동적으로 하나씩 줄어들어 자동으로
재배열되므로, 0, 1, 2, 3, ... 이런식으로 순차적 인덱싱으로 해서는 중간의 하나씩 아이템들이
삭제되지 않고 남습니다. 차선책은 다음과 같은 코드입니다.
void __fastcall TFrmMain::ClearFileList(void)
{
    for(int i=0; i<FileList->Count; i++)
    {
        delete (FLIST *)FileList->Items[0];
        FileList->Delete(0);
    }
}
이 코드에서는, 언제나 첫번째(인덱스 0) 아이템만 삭제하므로 중간에 빼먹지 않고 모두를
삭제할 수 있습니다. 원하는대로 이상없이 동작하기는 합니다만, 가장 마지막이 아닌 첫번째
아이템들을 삭제하므로 TList의 내부적으로 매번 재배열 작업이 이루어져서 속도가 다소
떨어집니다. 역시, 제가 보여드린 원래의 코드가 가장 효율적입니다.

이 코드를 다시 수정해볼 만한 여지는 남아있습니다. 보여주신 원래 소스의 struct나 findfirst,
그리고 스트링 연산등을 보면 전형적인 도스기반의 터보씨 코딩을 하셨는데, 물론 빌더는 터보씨의
함수들도 하위호환성을 제공하므로 이상없이 동작합니다만, 빌더에서 새로이 추가된 특징들을
이용하면 향후의 호환성이나 효율성 면에서 훨씬 나은 코드를 작성할 수 있습니다. 다소 시간이
남아서.. 그러한 코드를 다시 보여드립니다.

TStringList *FileList;

void __fastcall TFrmMain::FormCreate(TObject *Sender)
{
    FileList = new TStringList;
    if(FileList == NULL)  return;

    MakeFileList("c:\\*.*");
}

void __fastcall TFrmMain::FormDestroy(TObject *Sender)
{
    delete FileList;
}

void __fastcall TFrmMain::MakeFileList(AnsiString DirName)
{
    TSearchRec SR;
    int done;

    FileList->Clear();
    done = FindFirst(DirName, faAnyFile, SR);
    while(!done)
    {
        FileList->Add(SR.Name);
        done = FindNext(SR);
    }
}

훨씬 간결하죠? 속도 면에서도 이전의 도스방식 코드와 거의 차이나지 않을 뿐 아니라
(사실 아주! 조금쯤은 느릴 겁니다) 빌더에서 새로이 지원하는 특징들을 이용함으로써
코딩 중의 실수같은 것들을 많이 줄일 수 있습니다.

앞에서 보셨겠지만, TList는 활용도가 엄청 많습니다만, 사용하기에 그리 만만하지는
않은 클래스입니다. 시간을 한번 내어서 강좌를 올리는 것을 고려해보도록 하겠습니다.
TList를 제대로 설명하려면 거의 책의 한 챕터 정도 정도의 양이 되니 조금 부담스럽긴
합니다만, 워낙 자주 쓰이기도 하고 또 그만큼 실수도 하기 쉬우니까요.

그럼 이만...

+ -

관련 글 리스트
2037 [질문] TList로 파일리스트 만들어 출력하는데 문제가? 강민주 3486 1999/10/02
2038     Re:[질문] TList로 파일리스트 만들어 출력하는데 문제가? 박지훈.임프 3788 1999/10/02
Google
Copyright © 1999-2015, borlandforum.com. All right reserved.