Object Pascal · 기본
Object Pascal 첫걸음
컬렉션과 파일 - TList 와 TDictionary
Generics.Collections 의 TList·TDictionary·TObjectList(소유권), TStringList, 텍스트 파일 읽고 쓰기(TStringList.LoadFromFile·SaveToFile), 정렬
개발자KR · 원고 갱신
이 장에서 배우는 것
지금까지 만든 카페 프로그램은 메뉴 몇 개를 변수나 배열에 직접 적어 두고 돌렸다. 메뉴가 늘고 재고가 바뀌고 매출 기록이 쌓이면 개수가 미리 정해진 배열로는 다루기 어렵다. 프로그램을 끄면 데이터도 사라진다. 이 장에서는 개수가 늘고 줄어드는 자료를 담는 컬렉션(collection, 여러 값을 담는 그릇)과, 자료를 텍스트 파일에 저장하고 다시 읽는 방법을 다룬다.
- TList 로 값의 목록을 만들고 정렬한다.
- TDictionary 로 이름을 열쇠(key)로 삼아 값을 찾는다.
- TObjectList 가 객체를 소유한다는 것이 무슨 뜻인지 알고, 해제 책임이 누구에게 있는지 구분한다.
- TStringList 로 텍스트 파일을 읽고 쓰고 정렬한다.
문제 상황
카페 사장이 아침마다 메뉴와 가격을 손으로 고쳐 달라고 한다고 하자. 가격이 바뀔 때마다 소스를 고쳐 다시 컴파일하는 방식은 곤란하다. 메뉴는 파일에 두고 프로그램은 그 파일을 읽어야 한다. 하루 동안 팔린 금액은 몇 건이 될지 알 수 없다. 고정 길이 배열을 쓰면 100건으로 잡아 둔 칸이 모자라거나 남는다.
재고도 문제다. 메뉴 이름을 넣으면 남은 개수가 바로 나와야 한다. 배열을 처음부터 끝까지 훑는 방식은 코드가 길어지고 실수하기 쉽다. 마지막으로 메뉴 하나하나는 이름과 가격을 가진 객체인데, 객체를 목록에 넣었다면 누가 해제할지 정해야 한다. 앞 장에서 try finally 로 자원을 정리하는 법을 배웠으니 같은 원칙이 여기에도 적용된다. 이번에는 그 책임을 목록에 맡기는 방법을 본다.
컬렉션 고르기
제네릭이란
Generics.Collections 유닛의 TList, TDictionary, TObjectList 는 제네릭(generic) 클래스다. 제네릭은 담을 타입을 나중에 정하는 틀이다. 같은 TList 라도 정수를 담을 수도 있고 문자열을 담을 수도 있다. Free Pascal 의 objfpc 모드에서는 타입을 정할 때 specialize 라는 낱말을 앞에 붙이고, 이름을 하나 붙여 두고 쓰는 편이 읽기 좋다.
type
TIntList = specialize TList<Integer>;
TStockMap = specialize TDictionary<string, Integer>;
TMenuList = specialize TObjectList<TMenuEntry>;
Delphi 에서는 TList<Integer> 처럼 specialize 없이 쓰고, 유닛 이름도 System.Generics.Collections 다. 이 책의 환경에서는 위 형태를 쓴다.
| 컨테이너 | 담는 것 | 찾는 방법 | 카페 예 |
|---|---|---|---|
| TList | 같은 타입의 값 | 번호(0부터) | 팔린 금액 목록 |
| TObjectList | 객체(소유함) | 번호, 순회 | 메뉴 객체 목록 |
| TDictionary | 열쇠와 값의 짝 | 열쇠 | 메뉴 이름별 재고 |
| TStringList | 문자열 줄 | 번호, 이름 | 메뉴 파일의 줄들 |
TList: 늘어나는 값 목록
TList 는 값을 순서대로 담는다. Add 로 끝에 붙이고, Count 가 개수이며, List[i] 로 i 번째를 읽는다. 번호는 0부터 시작한다. Delete(i) 는 i 번째를 지우고, Sort 는 작은 값부터 정렬한다. 정수나 문자열처럼 크기를 비교할 수 있는 타입은 인수 없이 Sort 만 불러도 된다. 범위를 벗어난 번호를 쓰면 예외가 난다. for 값 in 목록 do 로 처음부터 끝까지 돌 수도 있다. 번호가 필요 없을 때는 이쪽이 실수할 여지가 적다.
TDictionary: 열쇠로 찾기
TDictionary 는 사전처럼 열쇠(key)를 알려 주면 값(value)을 돌려준다. 열쇠는 한 번만 넣을 수 있다. 넣는 방법은 둘이다.
Add(열쇠, 값): 이미 있는 열쇠를 넣으면 예외가 난다.AddOrSetValue(열쇠, 값): 없으면 넣고 있으면 값을 바꾼다.
값을 읽을 때는 TryGetValue(열쇠, 변수) 를 쓴다. 열쇠가 있으면 True 를 돌려주고 변수에 값을 채운다. 없으면 False 다. Stock['Muffin'] 처럼 바로 읽는 방법도 있지만, 열쇠가 없으면 예외가 난다. 있는지 확실하지 않을 때는 TryGetValue 가 안전하다. ContainsKey, Remove, Count 도 쓸 수 있다.
사전은 내부에서 열쇠를 해시(hash, 열쇠를 계산해 칸 번호로 바꾸는 방식)로 저장한다. 그래서 for 열쇠 in 사전.Keys do 로 돌 때 나오는 순서를 정할 수 없다. 넣은 순서나 가나다 순서가 아니다. 출력이 매번 같아야 하는 프로그램에서는 열쇠를 TStringList 에 옮겨 정렬한 뒤 출력한다. 완성 코드의 재고 출력이 이 방식이다.
TObjectList: 객체를 소유하는 목록
메뉴 하나를 이름과 가격을 가진 클래스로 만들면 목록에는 객체 자체가 아니라 객체를 가리키는 참조가 들어간다. 이 목록을 TList 로 만들면 목록을 해제해도 참조만 사라지고 객체는 메모리에 남는다. 객체를 직접 하나씩 Free 해야 하고, 빠뜨리면 메모리가 새어 나간다(memory leak).
TObjectList 는 이 책임을 목록이 진다. 만들 때 OwnsObjects 를 True 로 주면 목록이 객체를 소유한다. 소유한다는 것은 목록에서 빠지거나 목록이 해제될 때 객체도 함께 해제한다는 뜻이다. Delete(i), Clear, 목록의 Free 가 모두 해당한다. 객체를 목록에서 빼되 해제하고 싶지 않으면 Extract 를 쓴다. 이 경우 해제는 꺼낸 쪽의 책임이 된다.
소유권의 원칙은 하나다. 객체를 해제하는 주체는 정확히 하나여야 한다. 목록이 소유하는데 직접 Free 까지 하면 같은 객체를 두 번 해제하게 된다. 아무도 해제하지 않으면 새어 나간다.
텍스트 파일 읽고 쓰기
TStringList 와 파일
TStringList 는 Classes 유닛의 클래스로, 문자열을 한 줄씩 담는다. Add 로 줄을 더하고 Lines[i](기본 속성이라 Lines[i] 로 쓴다)로 읽는다. 이 클래스는 파일과 바로 이어진다. SaveToFile('이름') 은 담긴 줄을 파일로 쓰고, LoadFromFile('이름') 은 파일의 줄을 읽어 온다. 파일 열기와 닫기, 줄바꿈 처리는 클래스가 한다. 읽기 전에 있던 내용은 지워진다.
메뉴 파일을 이름=가격 형태로 쓰면 TStringList 가 이 형식을 이해한다. Names[i] 는 i 번째 줄에서 등호 앞부분을, ValueFromIndex[i] 는 뒷부분을 돌려준다. 가격은 파일에서 글자로 읽히므로 StrToInt 로 정수로 바꿔 쓴다.
정렬
Sort 는 문자열 줄을 사전순으로 정렬한다. 가격처럼 다른 기준으로 정렬하려면 CustomSort 에 비교 함수를 넘긴다. 이 함수는 두 줄의 번호를 받아 첫째가 앞서야 하면 음수, 같으면 0, 뒤서야 하면 양수를 돌려준다. 이 장에서는 두 번호의 가격을 빼서 큰 값이 앞에 오게 한다. 예제의 가격은 서로 달라서 같은 값끼리의 순서를 따지지 않아도 된다. 정렬 방식은 같은 값의 순서를 유지한다고 보장하지 않는다.
이 장의 예제는 메뉴 이름을 영문 알파벳으로만 썼다. 한글이 섞인 문자열을 이름순으로 정렬하려면 운영체제의 언어 설정에 따른 비교가 개입할 수 있어서, 별도의 주의가 필요하다.
| 항목 | Delphi | Free Pascal(objfpc) |
|---|---|---|
| 제네릭 타입 선언 | TList<Integer> | specialize TList<Integer> |
| 유닛 이름 | System.Generics.Collections | Generics.Collections |
| 비교 방식 지정 | 익명 함수 사용 가능 | Generics.Defaults 유닛과 비교자 필요 |
이 장은 정수에 대한 기본 정렬과 TStringList 의 CustomSort 만 쓰므로 Generics.Defaults 유닛을 쓰지 않는다.
완성 코드
메뉴 파일을 만들고 읽어 객체 목록을 채운 뒤, 주문을 처리하고 재고와 정산을 출력한다. 마지막에는 객체가 모두 해제됐는지 센다. 해제를 눈으로 확인하려고 클래스 안에서 살아 있는 객체 수를 세는 전역 변수를 두었다. 실무 코드에는 필요 없다.
main.pas
{$mode objfpc}{$H+}
program Main;
uses
SysUtils, Classes, Generics.Collections;
const
MenuFile = 'menu.txt';
ReportFile = 'report.txt';
Orders: array[0..6] of string =
('Latte', 'Americano', 'Latte', 'Latte', 'Mocha', 'Tea', 'Muffin');
type
TMenuEntry = class
private
FName: string;
FPrice: Integer;
public
constructor Create(const AName: string; APrice: Integer);
destructor Destroy; override;
property Name: string read FName;
property Price: Integer read FPrice;
end;
TMenuList = specialize TObjectList<TMenuEntry>;
TStockMap = specialize TDictionary<string, Integer>;
TIntList = specialize TList<Integer>;
var
LiveEntries: Integer = 0;
constructor TMenuEntry.Create(const AName: string; APrice: Integer);
begin
inherited Create;
FName := AName;
FPrice := APrice;
Inc(LiveEntries);
end;
destructor TMenuEntry.Destroy;
begin
Dec(LiveEntries);
inherited Destroy;
end;
procedure WriteSampleMenu(const FileName: string);
var
Lines: TStringList;
begin
Lines := TStringList.Create;
try
Lines.Add('Americano=3000');
Lines.Add('Latte=4000');
Lines.Add('Mocha=4500');
Lines.Add('Tea=2500');
Lines.SaveToFile(FileName);
finally
Lines.Free;
end;
end;
procedure LoadMenu(Menu: TMenuList; const FileName: string);
var
Lines: TStringList;
i: Integer;
begin
Lines := TStringList.Create;
try
Lines.LoadFromFile(FileName);
for i := 0 to Lines.Count - 1 do
Menu.Add(TMenuEntry.Create(Lines.Names[i],
StrToInt(Lines.ValueFromIndex[i])));
finally
Lines.Free;
end;
end;
function ByPriceDesc(List: TStringList; Index1, Index2: Integer): Integer;
begin
Result := StrToInt(List.ValueFromIndex[Index2])
- StrToInt(List.ValueFromIndex[Index1]);
end;
procedure ShowByPrice(const FileName: string);
var
Lines: TStringList;
i: Integer;
begin
Lines := TStringList.Create;
try
Lines.LoadFromFile(FileName);
Lines.CustomSort(@ByPriceDesc);
for i := 0 to Lines.Count - 1 do
WriteLn(Lines.Names[i], ' ', Lines.ValueFromIndex[i], '원');
finally
Lines.Free;
end;
end;
function FindEntry(Menu: TMenuList; const AName: string): TMenuEntry;
var
Entry: TMenuEntry;
begin
Result := nil;
for Entry in Menu do
if Entry.Name = AName then
begin
Result := Entry;
Exit;
end;
end;
procedure Sell(Menu: TMenuList; Stock: TStockMap; Sales: TIntList;
const AName: string);
var
Entry: TMenuEntry;
Left: Integer;
begin
Entry := FindEntry(Menu, AName);
if Entry = nil then
begin
WriteLn('메뉴에 없음: ', AName);
Exit;
end;
if not Stock.TryGetValue(AName, Left) then
Left := 0;
if Left <= 0 then
begin
WriteLn('품절: ', AName);
Exit;
end;
Stock[AName] := Left - 1;
Sales.Add(Entry.Price);
WriteLn('판매: ', AName, ' ', Entry.Price, '원');
end;
procedure ShowStock(Stock: TStockMap);
var
Names: TStringList;
Key: string;
i: Integer;
begin
Names := TStringList.Create;
try
for Key in Stock.Keys do
Names.Add(Key);
Names.Sort;
for i := 0 to Names.Count - 1 do
WriteLn(Names[i], ': ', Stock[Names[i]]);
finally
Names.Free;
end;
end;
procedure Settle(Sales: TIntList);
var
Amount, Total, i: Integer;
Line: string;
Report: TStringList;
begin
Sales.Sort;
Total := 0;
Line := '';
for Amount in Sales do
begin
Inc(Total, Amount);
if Line <> '' then
Line := Line + ' ';
Line := Line + IntToStr(Amount);
end;
WriteLn('판매 금액(오름차순): ', Line);
Report := TStringList.Create;
try
Report.Add(Format('count=%d', [Sales.Count]));
Report.Add(Format('total=%d', [Total]));
Report.Add(Format('average=%.2f', [Total / Sales.Count]));
Report.SaveToFile(ReportFile);
Report.Clear;
Report.LoadFromFile(ReportFile);
for i := 0 to Report.Count - 1 do
WriteLn(Report[i]);
finally
Report.Free;
end;
end;
var
Menu: TMenuList;
Stock: TStockMap;
Sales: TIntList;
Order: string;
i: Integer;
begin
WriteSampleMenu(MenuFile);
Menu := TMenuList.Create(True);
Stock := TStockMap.Create;
Sales := TIntList.Create;
try
WriteLn('== 메뉴 불러오기 ==');
LoadMenu(Menu, MenuFile);
WriteLn('메뉴 ', Menu.Count, '개, 살아 있는 객체 ', LiveEntries, '개');
WriteLn('== 가격 높은 순 ==');
ShowByPrice(MenuFile);
Stock.Add('Americano', 5);
Stock.Add('Latte', 2);
Stock.Add('Mocha', 3);
Stock.Add('Tea', 0);
WriteLn('== 판매 ==');
for Order in Orders do
Sell(Menu, Stock, Sales, Order);
WriteLn('== 재고 ==');
ShowStock(Stock);
WriteLn('== 정산 ==');
Settle(Sales);
WriteLn('== 소유권 ==');
WriteLn('Tea 삭제 전: ', LiveEntries, '개');
for i := Menu.Count - 1 downto 0 do
if Menu[i].Name = 'Tea' then
Menu.Delete(i);
WriteLn('Tea 삭제 후: ', LiveEntries, '개');
finally
Sales.Free;
Stock.Free;
Menu.Free;
end;
WriteLn('목록 해제 후: ', LiveEntries, '개');
DeleteFile(MenuFile);
DeleteFile(ReportFile);
end.
줄별 해설
상수와 타입. Orders 는 오늘 들어온 주문 일곱 건이다. 메뉴에 없는 Muffin 과 재고가 0인 Tea 를 일부러 넣었다. 타입 선언부에서 specialize 로 세 컬렉션에 이름을 붙였다. TObjectList 는 담는 타입이 클래스여야 하므로 TMenuEntry 를 넘겼다.
TMenuEntry. 생성자가 LiveEntries 를 하나 올리고 소멸자가 하나 내린다. 이 숫자가 0으로 돌아오면 만든 객체가 모두 해제된 것이다. 소멸자는 override 로 선언하고 마지막에 inherited Destroy 를 부른다.
WriteSampleMenu. TStringList 에 네 줄을 넣고 SaveToFile 로 쓴다. 실제 프로그램에서는 사장이 편집기로 만든 파일을 읽겠지만, 이 장은 예제만 돌려도 끝까지 실행되도록 프로그램이 파일을 먼저 만든다. TStringList 는 객체이므로 try finally 로 감싸 Free 한다.
LoadMenu. LoadFromFile 로 줄을 읽고, 각 줄에서 이름과 가격을 꺼내 TMenuEntry 를 만들어 목록에 넣는다. 이 객체를 따로 Free 하는 코드는 없다. 목록이 소유하기 때문이다.
ByPriceDesc 와 ShowByPrice. 파일을 다시 읽어 CustomSort 로 가격이 큰 줄을 앞에 둔다. 비교 함수는 모양이 정해져 있어서 매개변수 목록을 그대로 써야 한다. 호출할 때는 함수 이름 앞에 @ 를 붙여 함수의 주소를 넘긴다.
FindEntry. for Entry in Menu 로 메뉴를 훑어 이름이 같은 객체를 찾는다. 없으면 nil 을 돌려준다. 메뉴가 수십 개인 카페에서는 문제가 되지 않는 방식이다. 수만 개로 늘면 이름을 열쇠로 하는 사전을 따로 두는 편이 낫다.
Sell. 이 프로그램의 핵심이다. 순서는 세 단계다. 메뉴에 없으면 알리고 끝낸다. 재고를 TryGetValue 로 읽어서 열쇠가 없으면 0으로 본다. 재고가 없으면 품절을 알리고 끝낸다. 모두 통과하면 재고를 하나 줄이고 금액을 Sales 에 더한다. Exit 는 프로시저를 바로 끝낸다.
ShowStock. 사전의 열쇠를 TStringList 에 옮겨 Sort 한 다음, 정렬된 이름으로 값을 읽어 출력한다. 사전을 돌 때 나오는 순서는 믿을 수 없기 때문이다.
Settle. Sales.Sort 로 금액을 오름차순으로 정렬하고 합계와 이어 붙인 문자열을 만든다. 보고서는 이름=값 형태로 파일에 쓰고, Clear 로 비운 뒤 다시 읽어 출력한다. 평균은 Format('%.2f') 으로 소수 둘째 자리까지 고정해 출력이 매번 같다.
본문. 컬렉션 셋을 만든 뒤 한 개의 try finally 로 감싸 모두 해제한다. TObjectList 는 Create(True) 로 소유권을 명시했다. 뒤쪽 소유권 시연에서는 Delete 로 Tea 를 지우자 객체가 하나 줄고, Menu.Free 가 남은 객체를 모두 정리해 0이 된다. 목록을 앞에서부터 돌며 지우면 번호가 밀려서 건너뛰는 항목이 생기므로 downto 로 뒤에서 돈다. 끝에서 임시 파일 두 개를 지운다.
실행 결과
$ fpc main.pas
$ ./main
== 메뉴 불러오기 ==
메뉴 4개, 살아 있는 객체 4개
== 가격 높은 순 ==
Mocha 4500원
Latte 4000원
Americano 3000원
Tea 2500원
== 판매 ==
판매: Latte 4000원
판매: Americano 3000원
판매: Latte 4000원
품절: Latte
판매: Mocha 4500원
품절: Tea
메뉴에 없음: Muffin
== 재고 ==
Americano: 4
Latte: 0
Mocha: 2
Tea: 0
== 정산 ==
판매 금액(오름차순): 3000 4000 4000 4500
count=4
total=15500
average=3875.00
== 소유권 ==
Tea 삭제 전: 4개
Tea 삭제 후: 3개
목록 해제 후: 0개
실무에서 자주 틀리는 것
소유하는 목록의 객체를 또 Free 한다
TObjectList 에 넣은 객체는 목록이 해제한다. 직접 Free 하면 같은 객체를 두 번 해제하게 되고, 이때 프로그램은 오류로 멈추거나 이상한 값을 읽는다.
// 틀린 코드
Entry := TMenuEntry.Create('Scone', 3500);
Menu.Add(Entry);
Entry.Free; // 목록이 가진 참조는 이미 해제된 객체를 가리킨다
// 고친 코드
Menu.Add(TMenuEntry.Create('Scone', 3500)); // 해제는 목록에 맡긴다
반대 실수도 있다. 소유권이 필요한 자리에 TList 를 쓰면 아무도 해제하지 않아 객체가 새어 나간다. 객체를 담는 목록은 TObjectList 로 만들고 소유할지 여부를 Create 의 인수로 분명히 적는다.
사전을 돌 때 나오는 순서를 믿는다
// 틀린 코드: 출력 순서가 넣은 순서나 가나다 순서라고 가정한다
for Key in Stock.Keys do
WriteLn(Key, ': ', Stock[Key]);
// 고친 코드: 열쇠를 정렬해서 쓴다
Names := TStringList.Create;
try
for Key in Stock.Keys do
Names.Add(Key);
Names.Sort;
for i := 0 to Names.Count - 1 do
WriteLn(Names[i], ': ', Stock[Names[i]]);
finally
Names.Free;
end;
틀린 코드는 컴파일도 되고 한 번은 그럴듯하게 나올 수 있다. 하지만 정렬된 순서는 보장되지 않으므로, 순서가 필요한 출력은 정렬을 거친다.
없는 열쇠를 바로 읽는다
// 틀린 코드: 메뉴에 없는 이름이면 실행 중에 예외가 난다
Left := Stock['Muffin'];
// 고친 코드
if not Stock.TryGetValue('Muffin', Left) then
Left := 0;
열쇠가 있는지 확실하지 않을 때는 TryGetValue 를 쓴다. 열쇠가 반드시 있다는 조건이 이미 확인된 자리에서는 대괄호로 읽어도 된다.
파일이 없는 경우를 생각하지 않는다
LoadFromFile 은 파일이 없으면 예외를 낸다. 파일 이름에 경로를 쓰지 않으면 프로그램을 실행한 폴더 기준이다. 다른 폴더에서 실행하면 같은 코드가 파일을 찾지 못한다.
// 틀린 코드
Lines.LoadFromFile('menu.txt');
// 고친 코드
if FileExists('menu.txt') then
Lines.LoadFromFile('menu.txt')
else
WriteLn('menu.txt 가 없다. 실행한 폴더를 확인한다.');
파일이 있는지 확인한 뒤에도 읽는 도중 실패할 수 있다. 중요한 파일은 앞 장에서 배운 try except 로 감싸 사용자에게 알린다.
한눈에 보기
| 클래스 | 주요 메서드 | 기억할 점 |
|---|---|---|
| TList | Add, Delete, Count, Sort | 번호는 0부터, 기본 Sort 는 작은 값 먼저 |
| TObjectList | Add, Delete, Extract, Clear | Create(True) 면 객체까지 해제 |
| TDictionary | Add, AddOrSetValue, TryGetValue, Remove | 돌 때 순서가 정해져 있지 않다 |
| TStringList | Add, Sort, CustomSort, LoadFromFile, SaveToFile | 이름=값 줄은 Names 와 ValueFromIndex 로 나눈다 |
연습 문제
- 메뉴 파일을 읽어
Scone=3500줄을 더한 뒤 같은 파일에 다시 저장하는 프로시저AddScone을 쓰라. - 재고 사전의 Tea 가 이미 있든 없든 5를 더해 넣는 코드를 쓰라. 있으면 기존 값에 더하고, 없으면 5로 시작한다.
- 정수 목록 Sales 를 큰 금액부터 한 줄에 하나씩 출력하는 코드를 쓰라. 정렬 비교 함수는 쓰지 않는다.
- 메뉴 목록을
specialize TList<TMenuEntry>로 만들었더니 프로그램을 끝낸 뒤 LiveEntries 가 0이 되지 않았다. 원인과 고치는 방법을 설명하라.
정답과 해설
1번. 읽고, 더하고, 쓴다.
procedure AddScone(const FileName: string);
var
Lines: TStringList;
begin
Lines := TStringList.Create;
try
Lines.LoadFromFile(FileName);
Lines.Add('Scone=3500');
Lines.SaveToFile(FileName);
finally
Lines.Free;
end;
end;
LoadFromFile 이 기존 줄을 모두 읽어 오므로 Add 한 줄이 끝에 붙는다. SaveToFile 은 파일 전체를 다시 쓴다.
2번. TryGetValue 로 현재 값을 읽고 AddOrSetValue 로 넣는다.
if not Stock.TryGetValue('Tea', Left) then
Left := 0;
Stock.AddOrSetValue('Tea', Left + 5);
없으면 Left 가 0이라 5가 들어가고, 있으면 기존 값에 5가 더해진다. Add 를 쓰면 이미 있는 열쇠에서 예외가 나므로 이 자리에는 AddOrSetValue 가 맞다.
3번. 오름차순으로 정렬한 뒤 뒤에서부터 돈다.
Sales.Sort;
for i := Sales.Count - 1 downto 0 do
WriteLn(Sales[i]);
Sort 는 작은 값이 앞이므로, 마지막 번호부터 0까지 내려가면 큰 금액이 먼저 나온다.
4번. TList 는 객체를 소유하지 않는다. 목록을 Free 해도 목록이 가리키던 객체는 해제되지 않아 소멸자가 불리지 않고 LiveEntries 가 줄지 않는다. specialize TObjectList<TMenuEntry> 로 바꾸고 Create(True) 로 만들면 목록이 객체를 해제한다. TList 를 그대로 쓰려면 목록을 해제하기 전에 모든 객체를 직접 Free 해야 한다.
다음 장에서는 지금까지 만든 클래스, 컬렉션, 파일, 예외 처리를 합쳐 카페의 하루 정산 프로그램을 만든다.
READER FEEDBACK
질문·의견
내용에 관한 질문이나 더 나은 설명을 위한 의견을 남겨 주세요. 오탈자는 위의 제보 양식이 더 빨리 반영됩니다. 이 댓글은 원래 게시글과 같은 자리에 쌓입니다.
댓글 0
아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.