변수와 타입 - 선언부터 하는 언어
이 장에서 배우는 것
앞 장에서는 Pascal 프로그램이 program 머리, 선언부, begin과 end. 사이의 실행부로 이루어진다는 것을 보았다. 이 장은 그중 선언부를 채우는 방법을 다룬다. Pascal은 값을 쓰기 전에 이름과 종류를 먼저 적어 두어야 하는 언어다. 이 규칙을 처음에는 번거롭게 느낄 수 있다. 하지만 이 규칙 덕에 컴파일러가 숫자와 글자를 섞어 쓰는 실수를 실행 전에 찾아 준다. 이 장에서는 동네 카페 계산대의 주문 한 건을 변수로 옮기고, 소계·부가세·인원별 나눔 금액·컵 원가를 계산해 출력한다.
var블록과const블록으로 이름을 선언한다.Integer,Int64,Double,Currency,Boolean,Char,string일곱 가지 타입을 용도에 맞게 고른다.- 대입
:=와 비교=를 구분해서 쓴다. IntToStr,StrToInt,Trunc,Round로 타입 사이의 값을 바꾸고, 소수를 버리거나 반올림할 때 생기는 차이를 설명한다.
문제 상황
카페 계산대 프로그램을 만든다고 하자. 손님이 아메리카노 3잔과 라테 2잔을 주문했다. 점원은 소계를 구하고, 부가세 10%를 계산하고, 일행 8명이 나누어 낼 때 한 사람 몫을 알려 주어야 한다. 이 몫은 소수점 아래가 남는다. 마감 때는 한 달 치 매출을 합산하는데, 이 값은 일반 정수로 표현할 수 있는 범위를 넘을 수 있다. 포장 컵의 원가는 소수 둘째 자리까지 따져야 한다.
이 작업에는 서로 성격이 다른 값이 섞여 있다. 개수와 금액은 정수다. 비율과 나눗셈 결과는 소수다. 회원 여부는 참과 거짓이다. 사이즈 기호는 글자 하나다. 가게 이름은 글자 여러 개다. Pascal에서는 값마다 어떤 종류인지 선언부에 미리 적고, 컴파일러가 그 선언대로 값을 검사한다. 어떤 종류를 고르느냐에 따라 계산 결과가 달라지기도 한다. 이 장의 목표는 그 선택 기준을 익히는 것이다.
선언부: var 와 const
var 블록
변수(variable)는 값을 담아 두는 이름 붙은 칸이다. Pascal에서 변수는 var 예약어 아래에 이름: 타입; 모양으로 선언한다. 같은 타입이면 쉼표로 묶을 수 있다.
var
americanoQty, latteQty: Integer;
itemTotal: Integer;
title: string;
선언은 반드시 begin 앞에 와야 한다. 실행부 한가운데에서 변수를 새로 만들 수 없다. 선언부와 실행부가 분리된 것이 Pascal의 특징이다. 프로그램을 읽는 사람은 begin까지만 훑어도 이 프로그램이 어떤 값을 다루는지 알 수 있다.
타입은 칸의 크기와 담을 수 있는 값의 종류를 정한다. 아래 그림은 같은 프로그램에서 세 타입의 칸이 얼마나 다른 크기를 쓰는지 보여 준다.
이 책의 예제는 모두 첫머리에 {$mode objfpc}{$H+}를 적는다. {$mode objfpc}는 Free Pascal에게 Delphi와 비슷한 문법 모드로 컴파일하라고 지시한다. 이 모드에서 Integer는 32비트 정수다. Free Pascal의 기본 모드에서는 Integer가 16비트이므로 이 지시문을 빼먹으면 범위가 크게 달라진다. {$H+}는 string을 길이 제한이 없는 긴 문자열로 만든다. 이 둘은 한 쌍으로 기억해 두면 된다.
| 타입 | 크기 | 담는 값 | 카페 예 |
|---|---|---|---|
Integer | 4바이트 | 약 -21억 ~ 21억의 정수 | 주문 수량, 한 건의 소계 |
Int64 | 8바이트 | 약 ±922경의 정수 | 한 달 이상의 누적 매출 |
Double | 8바이트 | 소수(부동소수점, 약 15자리 정밀도) | 비율, 나눗셈 결과 |
Currency | 8바이트 | 소수 넷째 자리까지 정확한 고정소수 | 컵 원가 같은 소수 금액 |
Boolean | 1바이트 | True 또는 False | 회원 할인 대상 여부 |
Char | 1바이트 | 글자 하나(바이트 하나) | 사이즈 기호 'M' |
string | 길이에 따라 변함 | 글자의 나열 | 가게 이름, 영수증 문구 |
몇 가지를 덧붙인다. Double은 2진수로 소수를 근사하므로 0.1 같은 값을 정확히 담지 못한다. 그래서 돈처럼 한 푼의 오차도 문제가 되는 값에는 Currency를 쓴다. Currency는 내부적으로 정수에 10000을 곱해 저장하므로 소수 넷째 자리까지의 덧셈과 곱셈이 어긋나지 않는다. 원화는 소수가 필요 없어서 이 책 카페의 판매가는 Integer로 둔다. 소수가 붙는 원가에만 Currency를 쓴다.
Char는 바이트 하나다. 영문자와 숫자는 들어가지만 한글 한 글자는 UTF-8에서 3바이트를 차지하므로 Char에 들어가지 않는다. 한글은 string에 담는다. 같은 이유로 한글이 든 문자열에 Length를 쓰면 글자 수가 아니라 바이트 수가 나온다. 문자열은 다음 단계에서 본격적으로 다룬다.
Delphi와 다른 점: Delphi의
Char는 2바이트 유니코드 글자이고string도 유니코드 문자열이다. Free Pascal 3.2에서는 위 표의 크기를 기준으로 생각해야 한다.
전역 변수, 곧 프로그램 본체에 선언한 변수는 시작할 때 0, False, 빈 문자열로 채워진다. 그래도 값이 필요하면 항상 직접 대입하는 습관을 들이는 편이 좋다. 프로시저 안의 지역 변수는 이런 보장이 없어서, 초기화하지 않고 읽으면 쓰레기 값이 나온다. 지역 변수는 다음 단계의 주제다.
const 블록
상수(constant)는 프로그램이 도는 동안 바뀌지 않는 값에 이름을 붙인 것이다. const 아래에 이름 = 값;으로 쓴다. 변수와 달리 타입을 적지 않으면 값의 모양을 보고 컴파일러가 정한다.
const
ShopName = '골목 카페';
AmericanoPrice = 4500;
VatPercent = 10;
가격이 코드 곳곳에 4500으로 흩어져 있으면 가격이 오를 때 모든 곳을 찾아 고쳐야 하고, 하나라도 놓치면 계산이 어긋난다. 상수로 한 곳에 모아 두면 수정이 한 줄로 끝난다. 상수에 값을 대입하려고 하면 컴파일러가 오류를 낸다. 실수로 값이 바뀌는 것을 막아 주는 장치이기도 하다.
이름은 영문자나 밑줄로 시작하고 영문자·숫자·밑줄로 이어 쓴다. Pascal은 대소문자를 구분하지 않아서 itemTotal과 ITEMTOTAL은 같은 이름이다. 이 책은 읽기 쉽도록 변수는 소문자로 시작하고 상수는 대문자로 시작하는 관례를 따른다.
대입과 비교
Pascal에서 값을 변수에 넣는 연산자는 :=다. 오른쪽 식을 먼저 계산하고, 그 결과를 왼쪽 변수의 칸에 넣는다. 이전 값은 사라진다.
itemTotal := americanoQty * AmericanoPrice + latteQty * LattePrice;
같은지 묻는 연산자는 =다. 이것은 값을 바꾸지 않고 True 또는 False를 돌려준다. 다른지 물을 때는 <>, 크기를 비교할 때는 <, >, <=, >=를 쓴다. C 계열 언어에서 온 사람은 =가 대입이라는 습관이 있어 처음에 헷갈린다. Pascal에서는 대입과 비교의 모양이 달라서 둘을 혼동하면 컴파일 단계에서 오류가 난다.
| 연산자 | 의미 | 예 | 결과 |
|---|---|---|---|
:= | 대입 | n := 5 | 값이 칸에 들어간다 |
=, <> | 같다, 다르다 | n = 5 | Boolean |
+ - * | 덧셈, 뺄셈, 곱셈 | 3 * 4500 | 피연산자에 따라 정수 또는 소수 |
/ | 나눗셈 | 23900 / 8 | 항상 Double |
div, mod | 정수 몫, 나머지 | 23900 div 8 | 정수 |
나눗셈에는 주의할 점이 있다. /는 두 피연산자가 정수여도 결과가 Double이다. 그래서 Integer 변수에 itemTotal / 8을 바로 대입할 수 없다. 정수 몫이 필요하면 div를 쓰고, 나머지는 mod로 구한다. 23900원을 8명이 나누면 div는 2987, mod는 4이다. 8명이 2987원씩 내고 나면 4원이 남는다는 뜻이다.
반대로 정수를 Double 변수에 넣는 것은 자동으로 된다. 정수는 소수로 바꿔도 잃는 것이 없기 때문이다. 그러나 소수를 정수 변수에 넣을 때는 소수 부분을 어떻게 처리할지 프로그래머가 정해야 한다. 이것이 다음 절의 주제다.
형 변환
형 변환(type conversion)은 한 타입의 값을 다른 타입의 값으로 바꾸는 일이다. Pascal은 타입이 다른 값을 암묵적으로 섞는 데 엄격하다. 아래 그림은 이 장에서 쓰는 변환의 방향과 함수를 정리한 것이다.
문자열과 정수 사이
IntToStr은 정수를 문자열로, StrToInt는 문자열을 정수로 바꾼다. 둘 다 SysUtils 유닛에 있으므로 프로그램 첫머리에 uses SysUtils;를 적어야 한다. 유닛(unit)은 미리 만들어 둔 기능 묶음이며, 직접 만드는 방법은 뒤에서 다룬다. 영수증 문구에 금액을 붙이려면 문자열 덧셈 +에 문자열만 넣을 수 있으므로 IntToStr이 필요하다.
StrToInt('abc')처럼 숫자로 읽을 수 없는 문자열을 넘기면 프로그램이 오류로 멈춘다. 이 오류를 잡아 처리하는 방법(예외 처리)은 뒤에서 다룬다. 지금은 StrToIntDef를 쓰면 된다. 두 번째 인자로 실패했을 때 돌려줄 기본값을 받는다.
소수에서 정수로
Trunc는 소수 부분을 버리고 0 쪽으로 자른다. Round는 가장 가까운 정수로 반올림한다. 둘의 차이는 23900원을 8명이 나눈 2987.5에서 드러난다. Trunc(2987.5)는 2987이고 Round(2987.5)는 2988이다. 음수에서는 Trunc(-7.9)가 -7, Round(-7.9)가 -8이다.
정확히 .5인 값에서 Round의 동작은 따로 알아 두어야 한다. Free Pascal의 Round는 .5를 항상 올리지 않고, 결과가 짝수가 되는 쪽으로 보낸다. 은행가 반올림이라고 부르는 방식이다. 그래서 Round(2.5)는 2이고 Round(3.5)는 4이다. 학교에서 배운 4사5입과 다르므로, 금액을 반올림할 때는 이 규칙이 업무 기준에 맞는지 확인해야 한다.
| 함수 | 방향 | 예 | 주의 |
|---|---|---|---|
IntToStr | 정수 → 문자열 | IntToStr(23900) | SysUtils 필요 |
StrToInt | 문자열 → 정수 | StrToInt('12') | 숫자가 아니면 오류로 멈춘다 |
StrToIntDef | 문자열 → 정수 | StrToIntDef('abc', -1) | 실패하면 기본값(-1)을 돌려준다 |
Trunc | 소수 → 정수 | Trunc(2987.5) | 0 쪽으로 자른다 |
Round | 소수 → 정수 | Round(2987.5) | .5는 짝수 쪽으로 보낸다 |
소수를 화면에 보여 줄 때는 Format으로 자릿수를 고정한다. Format('%.2f', [vat])은 소수 둘째 자리까지 쓴다. 대괄호 안에 값을 넣는 모양이 처음에는 낯설겠지만 문자열 다루기에서 자세히 설명한다. 소수 자릿수를 고정하면 어느 컴퓨터에서 실행해도 출력이 같다. 공식 레퍼런스는 Free Pascal Reference Guide에서 확인할 수 있다.
완성 코드
아래는 위 내용을 한 파일에 모은 프로그램이다. 파일 이름은 main.pas다.
{$mode objfpc}{$H+}
program Main;
uses
SysUtils;
const
ShopName = '골목 카페';
AmericanoPrice = 4500;
LattePrice = 5200;
VatPercent = 10;
MemberMinTotal = 20000;
var
americanoQty, latteQty: Integer;
itemTotal: Integer;
monthSales: Int64;
vatRate: Double;
vat: Double;
perPerson: Double;
cupCost: Currency;
cupTotal: Currency;
isMember: Boolean;
sizeCode: Char;
nextCode: Char;
title: string;
qtyText: string;
qty: Integer;
begin
title := ShopName + ' 계산대';
Writeln(title);
americanoQty := 3;
latteQty := 2;
itemTotal := americanoQty * AmericanoPrice + latteQty * LattePrice;
Writeln('소계: ' + IntToStr(itemTotal) + '원');
vatRate := VatPercent / 100;
vat := itemTotal * vatRate;
Writeln('부가세: ', Format('%.2f', [vat]), '원');
Writeln('Integer 최댓값: ', High(Integer));
monthSales := itemTotal;
monthSales := monthSales * 100000;
Writeln('누적 매출: ', monthSales);
perPerson := itemTotal / 8;
Writeln('8명 나눔: ', Format('%.1f', [perPerson]));
Writeln('Trunc: ', Trunc(perPerson));
Writeln('Round: ', Round(perPerson));
Writeln('Round(2.5): ', Round(2.5), ' Round(3.5): ', Round(3.5));
Writeln('div: ', itemTotal div 8, ' mod: ', itemTotal mod 8);
cupCost := 85.35;
cupTotal := cupCost * 20;
Writeln('컵 원가 합계: ', Format('%.2f', [cupTotal]), '원');
isMember := itemTotal >= MemberMinTotal;
Writeln('회원 할인 대상: ', isMember);
Writeln('소계가 23900인가: ', itemTotal = 23900);
sizeCode := 'M';
nextCode := Chr(Ord(sizeCode) + 1);
Writeln('사이즈 ', sizeCode, ' 코드 ', Ord(sizeCode), ' 다음 ', nextCode);
qtyText := '12';
qty := StrToInt(qtyText) + 1;
Writeln('입력 문자열 ', qtyText, ' -> ', qty);
Writeln('잘못된 입력: ', StrToIntDef('abc', -1));
end.
줄별 해설
{$mode objfpc}{$H+}: 컴파일 모드와 긴 문자열 설정이다. 모든 파일의 첫머리에 둔다.program Main;: 프로그램의 이름이다. 파일 이름과 맞춰main.pas로 저장한다.uses SysUtils;:IntToStr,StrToInt,StrToIntDef,Format이 들어 있는 유닛을 불러온다.const블록: 가게 이름, 두 메뉴의 가격, 부가세율, 회원 할인 기준 금액을 상수로 둔다. 값이 바뀌면 이 블록만 고친다.var블록: 프로그램이 쓰는 모든 변수를 한곳에 적었다. 수량과 소계는Integer, 누적 매출은Int64, 비율과 나눗셈 결과는Double, 컵 원가는Currency다.title := ShopName + ' 계산대';: 상수와 문자열 리터럴을 이어 붙여 변수에 넣는다. 문자열 리터럴은 작은따옴표로 감싼다.itemTotal := ...: 3 × 4500 + 2 × 5200으로 23900이 된다. 곱셈이 덧셈보다 먼저 계산된다.Writeln('소계: ' + IntToStr(itemTotal) + '원');: 정수를 문자열로 바꾼 뒤 이어 붙인다.vatRate := VatPercent / 100;:/는Double을 돌려주므로 0.1이Double변수에 들어간다. 그 다음 줄에서 소계에 곱해 부가세를 구하고,Format('%.2f', ...)으로 소수 둘째 자리까지 출력한다.High(Integer):Integer가 담을 수 있는 가장 큰 값이다. 이 값이 21억 남짓이라는 것을 눈으로 확인한다.monthSales := itemTotal; monthSales := monthSales * 100000;: 먼저Int64변수에 값을 옮긴 다음 곱한다. 곱셈이Int64칸 안에서 이루어지므로 23900 × 100000 = 2390000000이 올바르게 나온다. 이 값은Integer최댓값보다 크다.perPerson := itemTotal / 8;: 2987.5가 된다. 이어서Trunc는 2987,Round는 2988을 돌려준다.Round(2.5)와Round(3.5)는 각각 2와 4로, .5를 짝수 쪽으로 보내는 규칙을 보여 준다.itemTotal div 8,itemTotal mod 8: 정수 몫 2987과 나머지 4를 구한다. 화면에 출력할 때Writeln은 정수를 문자열로 바꾸지 않고 그대로 받는다.cupCost := 85.35; cupTotal := cupCost * 20;:Currency곱셈은 소수 넷째 자리까지 정확하다. 결과 1707.00을Format으로 출력한다.isMember := itemTotal >= MemberMinTotal;: 비교 결과인Boolean이 변수에 들어간다.Writeln은Boolean을TRUE나FALSE로 출력한다.Writeln('소계가 23900인가: ', itemTotal = 23900);: 여기서=는 대입이 아니라 비교이므로 결과가TRUE다.Chr(Ord(sizeCode) + 1):Ord는 글자의 번호를 구한다.'M'은 77이다. 1을 더하고Chr로 다시 글자로 바꾸면'N'이 된다.StrToInt(qtyText) + 1: 문자열'12'를 정수 12로 바꾸어 1을 더한다. 마지막 줄의StrToIntDef('abc', -1)은 변환에 실패해 -1을 돌려준다.
실행 결과
$ fpc main.pas
$ ./main
골목 카페 계산대
소계: 23900원
부가세: 2390.00원
Integer 최댓값: 2147483647
누적 매출: 2390000000
8명 나눔: 2987.5
Trunc: 2987
Round: 2988
Round(2.5): 2 Round(3.5): 4
div: 2987 mod: 4
컵 원가 합계: 1707.00원
회원 할인 대상: TRUE
소계가 23900인가: TRUE
사이즈 M 코드 77 다음 N
입력 문자열 12 -> 13
잘못된 입력: -1
실무에서 자주 틀리는 것
비교 연산자를 대입으로 쓰기
다른 언어의 습관대로 =로 값을 넣으려는 경우다.
{ 틀린 코드 }
begin
itemTotal = 0;
end.
=는 비교 연산자이므로 이 문장은 아무 일도 하지 않는 식이어서 컴파일 오류가 난다. 대입은 :=다.
{ 고친 코드 }
begin
itemTotal := 0;
end.
Integer 범위를 넘기는 계산
매출을 Integer 변수에 모으다 보면 21억을 넘는 순간이 온다.
{ 틀린 코드 }
var
sales: Integer;
begin
sales := 23900;
sales := sales * 100000;
Writeln(sales);
end.
Free Pascal은 기본 설정에서 범위를 검사하지 않는다. 오류도 경고도 없이 값이 한 바퀴 돌아 엉뚱한 숫자가 출력된다. 큰 값을 다룰 변수는 처음부터 Int64로 선언하고, 곱셈을 하기 전에 그 변수로 값을 옮긴다. 완성 코드의 monthSales가 그 방식이다.
{ 고친 코드 }
var
sales: Int64;
begin
sales := 23900;
sales := sales * 100000;
Writeln(sales);
end.
돈 계산에 Double 쓰기와 소수 비교
Double은 0.1을 정확히 저장하지 못한다. 그래서 소수를 더한 결과를 =로 비교하면 기대와 다른 답이 나온다.
{ 틀린 코드 }
var
total: Double;
begin
total := 0.1;
total := total + 0.2;
if total = 0.3 then
Writeln('같다')
else
Writeln('다르다');
end.
이 코드는 다르다를 출력한다. 0.1 + 0.2가 0.30000000000000004에 가까운 값이기 때문이다. 금액에 소수가 있다면 Currency로 선언한다. Currency는 소수 넷째 자리까지 정확하므로 이 비교가 참이 된다.
{ 고친 코드 }
var
total: Currency;
begin
total := 0.1;
total := total + 0.2;
if total = 0.3 then
Writeln('같다')
else
Writeln('다르다');
end.
문자열과 숫자를 그대로 이어 붙이기
숫자를 문자열에 붙일 때 변환을 빼먹는 경우다. 입력 문자열을 숫자로 읽을 때 변환 실패를 가정하지 않는 경우도 같은 종류의 실수다.
{ 틀린 코드 }
begin
Writeln('합계: ' + itemTotal + '원');
qty := StrToInt('twelve');
end.
첫 줄은 문자열 + 정수라서 컴파일 오류가 난다. 둘째 줄은 컴파일은 되지만 실행하면 변환 오류로 프로그램이 멈춘다. 숫자는 IntToStr로 바꾸어 붙이고, 사용자 입력처럼 믿을 수 없는 문자열은 StrToIntDef로 읽는다.
{ 고친 코드 }
begin
Writeln('합계: ' + IntToStr(itemTotal) + '원');
qty := StrToIntDef('twelve', 0);
end.
한눈에 보기
| 주제 | 쓰는 법 | 고르는 기준 | 주의 |
|---|---|---|---|
| 변수 선언 | var 이름: 타입; | begin 앞에 모아 둔다 | 실행부에서 새로 선언할 수 없다 |
| 상수 | const 이름 = 값; | 바뀌지 않는 값, 여러 번 쓰는 값 | 대입하면 컴파일 오류 |
| 정수 | Integer, Int64 | 약 21억을 넘을 수 있으면 Int64 | 범위를 넘으면 조용히 틀린다 |
| 소수 | Double, Currency | 돈은 Currency | Double의 = 비교 |
| 참거짓, 글자 | Boolean, Char, string | 한글은 string | Char는 바이트 하나 |
| 대입과 비교 | :=, = | 값을 넣을 때 :=, 물을 때 = | 혼동하면 컴파일 오류 |
| 형 변환 | IntToStr, StrToInt, Trunc, Round | 방향에 맞는 함수를 고른다 | Round는 .5를 짝수 쪽으로 보낸다 |
연습 문제
- 카페에 아이스티(3800원)를 추가한다. 상수
IcedTeaPrice를 두고, 변수teaQty에 5를 대입한 뒤 아이스티 합계를합계: ...원형식으로 출력하는 프로그램의const,var, 실행부를 쓰시오. - 다음 두 줄의 출력을 예측하시오.
Writeln(Trunc(7.9), ' ', Round(7.9), ' ', Trunc(-7.9), ' ', Round(-7.9));
Writeln(17 div 5, ' ', 17 mod 5); - 다음 프로그램 조각에서 컴파일 오류가 나는 곳을 모두 찾아 고치시오.
var n: Integer; s: string;
begin n = 5; s := 'x' + n; end. - 체인 전체의 한 달 매출 3000000000원을 변수에 담아 30으로 나눈 정수 몫(일 평균)을 출력하려 한다. 알맞은 타입을 골라 쓰시오.
정답과 해설
-
const IcedTeaPrice = 3800; var teaQty: Integer; teaTotal: Integer; begin teaQty := 5; teaTotal := teaQty * IcedTeaPrice; Writeln('합계: ' + IntToStr(teaTotal) + '원'); end.IntToStr을 쓰려면uses SysUtils;가 필요하다. 합계는 19000이므로합계: 19000원이 출력된다. 가격은 상수, 수량은 변수로 나눈 것이 핵심이다. - 첫 줄은
7 8 -7 -8이다.Trunc는 0 쪽으로 자르고Round는 가까운 정수를 고르므로 7.9는 7과 8, -7.9는 -7과 -8이 된다. 둘째 줄은3 2다. 17은 5가 3번 들어가고 2가 남는다. - 오류는 두 곳이다.
n = 5;는 비교 연산자를 대입 자리에 쓴 것이므로n := 5;로 고친다.'x' + n은 문자열에 정수를 더한 것이므로'x' + IntToStr(n)으로 고친다. 후자를 쓰려면SysUtils가 필요하다. -
var monthSales: Int64; begin monthSales := 3000000000; Writeln(monthSales div 30); end.3000000000은
Integer최댓값 2147483647보다 크므로Int64를 쓴다. 출력은100000000이다. 정수 몫이 필요하므로/가 아니라div를 쓴다.