배열
배열 (Array)
Free Pascal은 Turbo Pascal처럼 배열을 지원해요. 다차원 배열, (비트)패킹 배열도 지원하고, 델파이식 동적 배열도 쓸 수 있어요.
출처: Arrays
본문
정적 배열 (Static arrays)
배열 정의에 범위가 포함되면 정적 배열이라고 불러요. 선언된 범위를 벗어난 인덱스로 요소에 접근하려 하면, 범위 검사(range checking)가 켜져 있을 때 런타임 에러가 발생해요. 다음은 유효한 배열 선언 예시예요.
Type
RealArray = Array [1..100] of Real;
유효한 인덱스는 1부터 100까지이고, 경계인 1과 100도 포함돼요. Turbo Pascal과 마찬가지로, 배열의 요소 타입이 다시 배열이면 두 배열을 하나의 다차원 배열로 합칠 수 있어요. 아래 선언은
Type
APoints = array[1..100] of Array[1..3] of Real;
다음 선언과 동일해요.
Type
APoints = array[1..100,1..3] of Real;
High와 Low 함수는 배열의 가장 왼쪽 인덱스 타입의 상한과 하한을 반환해요. 위 경우라면 100과 1이 되죠. 가능하면 항상 이 함수들을 쓰는 게 좋아요. 유지보수가 쉬워지거든요. 두 함수는 컴파일 타임에 평가되기 때문에 상수를 쓰는 것만큼 효율적이에요.
정적 배열 타입 변수끼리 대입하면 배열 전체 내용이 복사돼요. 다차원 배열에서도 마찬가지예요.
program testarray1;
Type
TA = Array[0..9,0..9] of Integer;
var
A,B : TA;
I,J : Integer;
begin
For I:=0 to 9 do
For J:=0 to 9 do
A[I,J]:=I*J;
For I:=0 to 9 do
begin
For J:=0 to 9 do
Write(A[I,J]:2,' ');
Writeln;
end;
B:=A;
Writeln;
For I:=0 to 9 do
For J:=0 to 9 do
A[9-I,9-J]:=I*J;
For I:=0 to 9 do
begin
For J:=0 to 9 do
Write(B[I,J]:2,' ');
Writeln;
end;
end.
이 프로그램의 출력은 두 개의 동일한 행렬이에요.
동적 배열 (Dynamic arrays)
버전 1.1부터 Free Pascal은 동적 배열을 지원해요. 이 경우 배열 범위를 생략해요.
Type
TByteArray = Array of Byte;
동적 배열 타입의 변수를 선언하면 초기 길이는 0이에요. 실제 길이는 표준 SetLength 함수로 설정해야 하는데, 이 함수가 배열 요소를 담을 메모리를 힙에 할당해 줘요.
다음 예제는 길이를 1000으로 설정해요.
Var
A : TByteArray;
begin
SetLength(A,1000);
SetLength 호출 후 유효한 인덱스는 0부터 999예요. 동적 배열 인덱스는 항상 0부터 시작하죠.
SetLength는 다차원 배열에도 쓸 수 있어요. 다음 예제는 "직사각형" 배열을 만들어요.
Var
A : Array of TByteArray;
begin
SetLength(A,10,100);
SetLength 호출 후 첫 번째 차원의 유효 인덱스는 09, 두 번째 차원은 099예요.
정적 다차원 배열과 달리 동적 배열은 반드시 "직사각형"일 필요가 없어요. 각 요소가 서로 다른 길이를 가질 수 있죠.
var
a: array of array of array of LongInt;
i, j, k: LongInt;
begin
SetLength(a, 10, 5);
SetLength(a[5], 3);
for i := Low(a) to High(a) do
for j := Low(a[i]) to High(a[i]) do begin
SetLength(a[i, j], i * 10 + j);
for k := Low(a[i, j]) to High(a[i, j]) do
a[i, j, k] := i * 10000 + j * 100 + k;
end;
for i := Low(a) to High(a) do begin
for j := Low(a[i]) to High(a[i]) do begin
for k := Low(a[i, j]) to High(a[i, j]) do
Writeln(a[i, j, k]);
Writeln('-------');
end;
Writeln('=======');
end;
end.
배열 길이는 할당된 메모리 바이트가 아니라 요소 수로 설정된다는 점에 주의하세요. (우연히 같을 수도 있지만요.) 할당되는 메모리 양은 배열 크기에 요소 하나의 크기를 곱한 값이에요. 메모리는 현재 프로시저나 함수를 벗어날 때 해제돼요.
배열 크기를 조정하는 것도 가능해요. 이 경우 새 크기에 맞는 만큼의 요소가 유지돼요. 배열을 0으로 리사이즈하면 사실상 변수가 초기화돼요.
항상 그렇듯, 현재 배열 길이에 없는 인덱스로 요소에 접근하려 하면 런타임 에러가 발생해요.
동적 배열은 참조 카운트(reference counted)돼요. 하나의 동적 배열 타입 변수를 다른 변수에 대입하면 두 변수가 같은 배열을 가리키게 돼요. AnsiString과 달리 한 배열의 요소에 대입한 것이 다른 배열에도 반영돼요. copy-on-write가 없기 때문이죠. 다음 예제를 보세요.
Var
A,B : TByteArray;
begin
SetLength(A,10);
A[0]:=33;
B:=A;
A[0]:=31;
두 번째 대입 이후 B의 첫 번째 요소도 31을 담게 돼요.
이건 다음 예제의 출력에서도 확인할 수 있어요.
program testarray1;
Type
TA = Array of array of Integer;
var
A,B : TA;
I,J : Integer;
begin
Setlength(A,10,10);
For I:=0 to 9 do
For J:=0 to 9 do
A[I,J]:=I*J;
For I:=0 to 9 do
begin
For J:=0 to 9 do
Write(A[I,J]:2,' ');
Writeln;
end;
B:=A;
Writeln;
For I:=0 to 9 do
For J:=0 to 9 do
A[9-I,9-J]:=I*J;
For I:=0 to 9 do
begin
For J:=0 to 9 do
Write(B[I,J]:2,' ');
Writeln;
end;
end.
이 프로그램의 출력은 숫자 행렬 하나, 그리고 그 행렬을 대칭 반전한 행렬이에요.
앞서 언급했듯 동적 배열은 참조 카운트 돼요. 앞 예제 중 하나에서 A가 범위를 벗어나고 B는 벗어나지 않았다면, 배열은 아직 해제되지 않아요. A(그리고 B)의 참조 카운트가 1 감소할 뿐이죠. 참조 카운트가 0이 되는 순간 배열 내용을 위해 할당된 메모리가 해제돼요.
SetLength 호출은 반환된 배열의 참조 카운트가 1이 되도록 보장해요. 즉 두 동적 배열 변수가 같은 메모리를 가리키고 있다면, setlength 호출 이후에는 더 이상 그러지 않아요.
program testunique;
Type
TA = array of Integer;
var
A,B : TA;
I : Integer;
begin
Setlength(A,10);
For I:=0 to 9 do
A[I]:=I;
B:=A;
SetLength(B,6);
A[0]:=123;
For I:=0 to 5 do
Writeln(B[I]);
end.
표준 Copy 함수로 배열을 복사하거나 리사이즈할 수도 있어요. 문자열의 copy 함수처럼 동작하죠.
program testarray3;
Type
TA = array of Integer;
var
A,B : TA;
I : Integer;
begin
Setlength(A,10);
For I:=0 to 9 do
A[I]:=I;
B:=Copy(A,3,6);
For I:=0 to 5 do
Writeln(B[I]);
end.
Copy 함수는 배열의 여섯 요소를 새 배열로 복사해요. 배열의 인덱스 3(즉 네 번째 요소)부터 시작하죠.
Length 함수는 배열의 요소 수를 반환해요. 동적 배열에서 Low는 항상 0을 반환하고, High는 Length-1, 즉 허용되는 가장 큰 배열 인덱스를 반환해요.
동적 배열 타입 호환성
Object Pascal은 타입이 엄격한 언어예요. 기술적으로 구별되는 두 타입도 특정 상황에서는 대입 호환(assignment compatible)으로 간주돼요. 동적 배열은 같은 요소 타입을 사용할 때 대입 호환이 돼요. 즉 다음 코드는 컴파일돼요.
{$mode objfpc}
Type
TA = Array of Integer;
TB = Array of Integer;
Var
A : TA;
B : TB;
begin
SetLength(A,1);
A[0]:=1;
B:=A;
end.
그런데 정수와 워드 타입이 대입 호환이라도 다음 코드는 컴파일되지 않아요.
{$mode objfpc}
Type
TA = Array of Word;
TB = Array of Integer;
Var
A : TA;
B : TB;
begin
SetLength(A,1);
A[0]:=1;
B:=A;
end.
동적 배열 생성자
Free Pascal 3.0 버전부터 동적 배열 타입은 생성자를 가져요. 이것은 내장(intrinsic)이라 컴파일러가 제공하죠. 2.6.4 버전까지는 동적 배열을 초기화하는 유일한 방법이 다음과 같았어요.
Type
TIntegerArray = Array of Integer;
var
A : TIntegerArray;
begin
SetLength(A,3);
A[0]:=1;
A[1]:=2;
A[3]:=3;
Writeln(Length(A));
end.
3.0 버전부터는 생성자 같은 문법으로 동적 배열을 초기화할 수 있어요. 생성자 이름은 Create이고, 배열 타입의 요소 타입에 해당하는 가변 개수의 파라미터를 받아요. 즉 위 초기화는 이제 이렇게 할 수 있죠.
Type
TIntegerArray = Array of Integer;
var
A : TIntegerArray;
begin
A:=TIntegerArray.Create(1,2,3);
Writeln(Length(A));
end.
이 방식은 타입이 만들어진 동적 배열에서만 동작한다는 점에 주의하세요. 즉 다음 코드는 동작하지 않아요.
var
A : Array of Integer;
begin
A:=Array of Integer.Create(1,2,3);
Writeln(Length(A));
end.
이 접근 방식은 다차원 배열에도 재귀적으로 동작해요.
Type
TIntegerArray = Array of Integer;
TIntegerArrayArray = Array of TIntegerArray;
var
A : TIntegerArrayArray;
begin
A:=TIntegerArrayArray.Create(TIntegerArray.Create(1,2,3),
TIntegerArray.Create(4,5,6),
TIntegerArray.Create(7,8,9));
Writeln('Length ',length(A));
end.
그런데 생성자는 런타임에 코드가 실행되므로, 초기화된 변수 문법에는 쓸 수 없어요. 즉 다음 코드는 동작하지 않아요.
Type
TIntegerArray = Array of Integer;
var
A : TIntegerArray = TIntegerArray.Create(1,2,3);
begin
Writeln('Length ',length(A));
end.
동적 배열 상수 표현식
컴파일러 3.2 버전부터는 명시적 대입 또는 초기화된 변수에서 배열 표현식으로 배열을 구성할 수 있어요. 다만 표현식은 달라요. 대입문에서는 set 표현식처럼 보이고, 초기화된 변수에서는 고정 길이 상수 배열과 같은 문법을 써야 해요.
Type
TIntegerArray = Array of Integer;
var
A : TIntegerArray = (1,2,3);
B : TIntegerArray;
begin
B:=[3,4,5];
end.
배열 패킹과 언패킹
배열은 packed와 bitpacked로 만들 수 있어요. 인덱스 타입과 요소 타입은 같지만 패킹 방식이 다른 두 배열 타입은 대입 호환되지 않아요.
하지만 pack 루틴으로 일반 배열을 bitpacked 배열로 변환할 수 있어요. 반대 연산도 가능해요. unpack 루틴으로 bitpacked 배열을 일반 패킹된 배열로 변환할 수 있죠.
Var
foo : array [ 'a'..'f' ] of Boolean
= ( false, false, true, false, false, false );
bar : bitpacked array [ 42..47 ] of Boolean;
baz : array [ '0'..'5' ] of Boolean;
begin
pack(foo,'a',bar);
unpack(bar,baz,'0');
end.
pack과 unpack 루틴에 대한 자세한 내용은 system 유닛 레퍼런스에서 확인할 수 있어요.
더 알아보기
동적 배열은 참조 카운트와 메모리 해제 동작이 AnsiString과 다르다는 점을 꼭 기억하세요. 대입 후에도 같은 메모리를 공유하므로 의도치 않은 변경이 번질 수 있어요.