보통 프로그램은 데이타와 그것을 관리하는 코드로 이루어져 있습니다.
객체 지향적인 입장에서 보면
데이타 객체와 데이타 객체를 관리하는 관리 클래스로 구성되어 있다고 말할 수도 있습니다.
이 둘은 하나일 수도 있고 둘로 나뉘어져 있을 수도 있고 또 여러개로 나뉘어져 있을 수도 있습니다.
데이타와 데이타를 관리하는 클래스를 묶어서 코딩하는 것은
객체지향적이고 유지보수에 좋기 때문에, 실무에서는 매우 빈번하게 사용됩니다.
하지만 아직 초보이거나 데이타와 관리 클래스를 묶어서 쓰는 기본 방식에 익숙지 않으면
데이타와 데이타를 다루는 기능이 소스의 여기저기 산개하게 됩니다.
스파게티 코드가 되어 나중에 유지보수 할때 답답하게 됩니다.
그런 의미에서 가장 가벼운 방식이면서도
여러가지 프로젝트에 유용하게 많이 사용될 수 있는 예를 하나 만들어 보겠습니다.
여기서 눈여겨 봐야 할 것은 전체 구조입니다.
그리고 네이밍 즉 명칭을 어떻게 붙이는가가 생각 이상 중요하니, 좋은 작명가가 되도록 노력해야 합니다.
예로 학생 데이타를 다루는 프로그램을 만들어 보겠습니다.
학생 데이타는 구조체로 TStudent 로 명칭을 붙입니다.
이 데이타는 그대로 파일로 저장도 되어야 하기 때문에 기본 형만으로 정의 합니다.
struct TStudent
{
bool Active; // 레코드 존재유무.
int No; // 학번. 유일키로 쓰는 항목.
char Name[100];
int Age;
char Job[100];
TStudent()
{
Clear();
}
void Clear()
{
//Name = ""; // String 객체라면 이렇게 먼저 해제해 주어야 한다.
ZeroMemory(this, sizeof(*this));
}
};
대략 이런 식으로 구성했습니다.
눈 여겨 봐야 할 것은 구조체이지만, 구조체의 데이타와 직접 관련 있는 기능은
바로 이 구조체에 메소드를 만들어 기능을 추가합니다.
대부분의 경우 구조체를 모두 0으로 초기화 하기 때문에, ZeroMemory... 로 초기화 시킵니다.
항목을 일일이 초기화 하지 않고 한방에 초기화 하기 때문에 매우 편리합니다.
TStudent 생성자에 Clear() 메소드를 호출하게 했는데,
이는 TStudent가 메모리에 자리 잡는 즉시 초기화 되게 한 것입니다.
이는 로칼 변수로 TStudent 선언된 경우라도 내용이 깔끔하게 초기화 시키기 때문에, 이런 방식의 코드가 좋습니다.
만일 구조체 내에 String Name; 식으로 기본형이 아닌 객체를 넣는 경우는,
Clear 할때 객체를 먼저 해제해주는 작업을 선행해야 합니다.
여기서 String Name; 을 쓰지 않고 char Name[100]; 으로 쓴 것은,
이 구조체 자체를 파일로 저장하기 위해서입니다.
한방에 저장하고 읽어 낼려면 레코드 내에 데이타가 자리 잡아야 하기 때문입니다.
그러면 이 데이타를 관리하는 클래스를 만들어야 하는데,
이는
class CStudent
{
public:
TStudent *Data;
int Size;
public:
CStudent()
{
Data = NULL;
Size = 0;
}
Add(...
Delete(...
};
식으로 만들면 됩니다.
관리 클래스는 TList 클래스를 연상하면 됩니다.
그기에 필요한 기능을 덧붙인 것으로 보면 되는데,
여기 제가 만든 예제에서는 TList 를 상속 받거나 하진 않고
독립적으로 구성되어 있습니다.
TList의 경우는 한 아이템당 4바이트 포인트만 수용할 수 있기 때문에
데이타 레코드는 별도로 메모리를 할당 받고 해제하는 작업을 동반하게 마련입니다.
만일 너무 많은 할당과 해제가 일어난다면 성능에도 좋지 않고, 메모리 단편화 문제가 생깁니다.
이는 형 자체를 직접 저장할 수 없었던 델파이의 레코드 다룸 방식이기 때문에,
C++에는 굳이 그렇게 할 필요가 없습니다.
물론 그렇게 성능이나 메모리에 민감한 프로그램이 아니라면 TList 식의 구성도 무방합니다.
요즘은 하드웨어 성능이 좋아서,
코딩이 하드웨어에 최적화 되지 않아도 동작에 큰 문제가 없는 경우가 대부분입니다.
데이타가 배열 형태이면 예제처럼 메모리를 한방에 할당 받아서 쓰는 방식이 좋고,
데이타가 map list 등의 형태 같으면 STL을 쓰는 것이 좋습니다.
물론 이 경우도 예로 보인 데이타와 데이타 관리 클래스, 데이타 관리 클래스의
기본 메소드는 거의 차이가 없는 일정한 구조를 가지게 됩니다.
아래에 있는 하나의 소스에 모든 것을 다 구현했지만
UI와 비지니스 로직이 그런대로 잘 분리되어 있습니다.
그래서 고수가 아니라면 코드를 잘 눈여겨 보면 나름대로 도움이 될 것입니다.
데이타는 TStudent,
클래스는 CStudent,
생성된 객체는 Student.
여기서 네이밍의 일관성을 볼수 있습니다.
뭘 어떻게 하던지 간에 프로그래머 마음이지만
자신도 뭔가 일관성 있는 네이밍 부여 규칙을 가지는게 좋습니다.
필자의 경우는 일반적으로 데이타는 T로 시작하고
클래스는 C로 시작하고, 객체는 대문자를 선행하는 단어 명칭을 씁니다.
예제에서는 학생의 학번, 이름, 나이를 입력받고
리스트에 이름과 나이를 표시하고, 학번으로 지울 수도 있게 되어 있습니다.
리스트에는 이름과 나이 데이타만 표시되고, 각 Row의 Data 프로퍼티에
키로 사용된 학번을 저장해 놓습니다.
그러면 이 키로 Student 객체를 통해 원래의 데이타를 찾을 수 있게 됩니다.
간단하지만 하나의 기본 구조를 가진 프로그램이 됩니다.
실무에서는 이것보다 훨씬 많은 기능이 들어가는 경우가 많습니다.
프로그램 종료시 입력해 놨던 값이 자동으로 저장되고
다시 프로그램을 실행하면 저장해 놨던 데이타를 읽어 CStudent 클래스 내에 적제하고
화면 리스트 컨트롤을 통해 표시합니다.
아래는 전체 소스입니다.
하나의 간단한 예일 뿐 대단한 기능이 있는 것은 아닙니다.
//---------------------------------------------------------------------------
#include
#pragma hdrstop
#include "Unit1.h"
#include "TFILE.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
//---------------------------------------------------------------------------
#define INDEX_NOTFOUND (-1)
// 데이타 구조체 (레코드)
struct TStudent
{
bool Active; // 레코드 존재유무.
int No; // 학번. 유일키로 쓰는 항목.
char Name[100];
int Age;
char Job[100];
TStudent()
{
Clear();
}
void Clear()
{
//Name = ""; // String 객체라면 이렇게 먼저 해제해 주어야 한다.
ZeroMemory(this, sizeof(*this));
}
};
// 관리 클래스
class CStudent
{
public:
TStudent *Data;
int Size;
public:
CStudent()
{
Data = NULL;
Size = 0;
}
CStudent(int size)
{
Data = NULL;
SetSize(size);
}
void SetSize(int size)
{
if (Data) // 사이즈가 변했을때 기존 내용을 옮겨야 한다면, 이 부분에 옮기는 처리를 추가해야 한다.
delete[] Data;
Data = new TStudent[size];
Size = size;
}
~CStudent()
{
if (Data)
delete[] Data;
}
// R:index
int Add(TStudent& data)
{
if (!Data)
return INDEX_NOTFOUND;
int idx = FindBlank();
if (idx >= 0)
Data[idx] = data;
return idx;
}
void Delete(int idx)
{
if (!Data)
return;
if (idx < Size)
Data[idx].Clear();
}
// R:index
int FindNo(int No)
{
for(int c = 0; c < Size; c++)
{
if (Data[c].Active && Data[c].No == No)
return c;
}
return INDEX_NOTFOUND;
}
TStudent *Find(int No)
{
for(int c = 0; c < Size; c++)
{
if (Data[c].Active && Data[c].No == No)
return Data + c;
}
return NULL;
}
// R:index
int FindBlank()
{
for(int c = 0; c < Size; c++)
{
if (Data[c].Active == false)
return c;
}
return INDEX_NOTFOUND;
}
// R:index
int FindName(String name)
{
for(int c = 0; c < Size; c++)
{
if (name == Data[c].Name)
return c;
}
return INDEX_NOTFOUND;
}
void Load(char *filename)
{
TFILE file(filename, "rb");
if (file.fp == NULL)
return;
int filesize = file.GetFileSize();
SetSize(filesize / sizeof(TStudent));
file.fread(Data, filesize, 1);
}
void Save(char *filename)
{
TFILE file(filename, "wb");
if (file.fp == NULL)
return;
file.fwrite(Data, Size * sizeof(TStudent), 1);
}
};
CStudent Student(100);
//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
//---------------------------------------------------------------------------
void __fastcall TForm1::FormCreate(TObject *Sender)
{
Student.Load("Student.dat");
for(int c = 0; c < Student.Size; c++)
if (Student.Data[c].Active)
AddStudent(Student.Data[c]);
}
//---------------------------------------------------------------------------
void __fastcall TForm1::FormClose(TObject *Sender, TCloseAction &Action)
{
Student.Save("Student.dat");
}
//---------------------------------------------------------------------------
void __fastcall TForm1::BitBtn1Click(TObject *Sender)
{
TStudent st;
st.No = EditNo->Text.ToIntDef(0);
strcpy(st.Name, EditName->Text.c_str());
st.Age = EditAge->Text.ToIntDef(0);
st.Active = true;
if (st.No == 0 || st.Name[0] == 0 || st.Age == 0)
{
ShowMessage("제대로 입력하시오.");
return;
}
if (Student.FindNo(st.No) != INDEX_NOTFOUND)
{
ShowMessage("이미 있는 학번이오");
return;
}
if (Student.Add(st) == INDEX_NOTFOUND)
{
ShowMessage("overflow");
return;
}
AddStudent(st);
}
void TForm1::AddStudent(TStudent& st)
{
TListItem *item = LV1->Items->Add();
item->Caption = st.Name;
item->SubItems->Add(st.Age);
item->Data = (void *)st.No;
}
//---------------------------------------------------------------------------
void __fastcall TForm1::LV1Change(TObject *Sender, TListItem *Item,
TItemChange Change)
{
int itemindex = LV1->ItemIndex;
if (itemindex < 0)
return;
int No = (int)LV1->Items->Item[itemindex]->Data;
int idx = Student.FindNo(No);
LNo->Caption = String(Student.Data[idx].Name) + " 의 학번은 " + Student.Data[idx].No;
}
//---------------------------------------------------------------------------
void __fastcall TForm1::BitBtn2Click(TObject *Sender)
{
int No = EditNo2->Text.ToIntDef(0);
int idx = Student.FindNo(No);
if (idx == INDEX_NOTFOUND)
{
ShowMessage("없는 학번이오");
return;
}
TListItems *items = LV1->Items;
for(int c = 0; c < items->Count; c++)
{
if (items->Item[c]->Data == (void *)Student.Data[idx].No)
items->Item[c]->Delete();
}
Student.Delete(idx);
}
//---------------------------------------------------------------------------
첨부 파일은 여기에 사용한 소스가 들어 있습니다.
그럼.
그리고 일반 자료 구조는 stl이나 boost 정도만 익히고 있는 것이 좋습니다. map, hash_map 정도만 알아도 웬만한 검색 최적화는 이루어질 수가 있습니다.
그리고 보통 class는 대문자로 시작, instance(object)는 소문자로 시작하는 것이 요즘 추세더라구요.