소개
Sunny Snacks에는 고객이 같은 간식을 다시 사더라도 구매 한 건씩 모두 남기는 영수증이 필요합니다. 먼저 상품 한 개의 완성된 예제를 실행한 다음, 구매를 따로 저장하고 각 행의 단가, 소계, 전체 합계를 출력하는 C 프로그램을 완성합니다.
C의 변수, 조건문, 반복문, 배열, 함수와 기본적인 터미널 입력 및 경로를 알고 있어야 합니다. 이 실습에서는 필요한 구조체와 텍스트 출력의 세부 사항을 소개합니다. 브라우저 환경에는 컴파일러와 Workspace 터미널이 준비되어 있습니다. 금액은 합성 데이터인 정수 센트 값이며 결제나 회계 시스템이 아닙니다. 읽기와 실습 시간은 저자의 예상치이며 초보자를 대상으로 측정하지 않았습니다.
상품 한 개의 영수증 읽기
이 단계에서는 작은 완성 예제를 실행하고 상품 한 개 구매를 두 개 구매로 바꿉니다.
Workspace 탭을 여세요. 터미널은 /home/labex/project/receipts에서 시작합니다. cd는 디렉터리를 바꾸고, pwd는 현재 디렉터리를 표시하며, ls는 파일 목록을 보여 줍니다. 경로나 이름을 잘못 입력했을 때도 이 명령으로 복구할 수 있습니다.
cd /home/labex/project/receipts
pwd
ls
sample.c는 상품 한 개의 완성 예제입니다. till.c는 다음 단계에서 사용할 여러 상품용 미완성 프로그램입니다. 카탈로그, 파서, 입력 드라이버는 준비된 파일이므로 바꾸지 마세요. cat은 텍스트 파일을 편집하지 않고 표시합니다.
cat sample.c
예제의 quantity 변수는 1로 시작합니다. unit_cents는 125로, 통화 단위 하나와 25센트입니다. 곱셈으로 센트 단위 소계를 계산합니다. 계산 내내 이 단위를 유지하세요. 100으로 정수 나눗셈을 하면 정수 부분을 얻고 % 100은 나머지를 구합니다. printf에서 %d는 정수, %s는 끝이 표시된 C 문자열을 출력하고, %02d는 소수 부분을 두 자리로 채우며, \n은 줄을 끝냅니다. 따라서 부동소수점 연산 없이 250센트를 2.50으로 표시합니다.
프로그램을 컴파일하세요. gcc는 C 컴파일러이고, -std=c11은 C11을 선택하며, -Wall -Wextra는 경고를 켜고, -o sample은 실행 파일 이름을 정합니다. ./sample은 현재 디렉터리의 해당 파일을 실행합니다.
gcc -std=c11 -Wall -Wextra sample.c -o sample
./sample
완전한 정상 결과는 다음과 같습니다.
SUNNY SNACKS
ID | ITEM | QTY | UNIT | SUBTOTAL
101 | Oat bar | 1 | 1.25 | 1.25
TOTAL | 1.25
ID는 상품 식별자이며 101은 이 카탈로그 항목을 가리킵니다. UNIT은 상품 하나의 가격이고, SUBTOTAL은 이 구매의 수량에 단가를 곱한 값입니다. 세로 막대는 읽기 쉬운 열을 구분합니다. 마지막 TOTAL은 모든 구매의 소계를 합하며, 여기서는 구매가 한 건입니다.
수량을 2로 바꾸세요. 다음 sed 명령은 정확히 일치하는 이전 텍스트를 새 텍스트로 바꾸고, -i는 변경을 저장합니다. 작은따옴표는 치환 내용을 하나의 셸 인수로 유지합니다.
sed -i 's/int quantity = 1;/int quantity = 2;/' sample.c
편집 후 다시 컴파일하세요. 소스 코드를 바꿔도 이전 실행 파일이 자동으로 갱신되지 않습니다. 셸 연산자 >는 표준 출력을 파일로 리디렉션합니다. C 프로그램이 시작되기 전에 파일을 열거나 기존 내용을 잘라 내므로 지정한 새 출력 이름을 사용하세요.
gcc -std=c11 -Wall -Wextra sample.c -o sample
./sample > sample-receipt.txt
cat sample-receipt.txt
이제 수량은 2이고 소계와 합계는 모두 2.50이며 단가는 1.25 그대로입니다. 125 × 2 = 250센트를 직접 확인하세요. sample.c, 실행 파일 sample, sample-receipt.txt를 보관합니다.
파일이 없다면 배운 cd로 돌아가 ls로 이름을 확인하세요. 컴파일러가 오류를 알리면 표시된 행을 수정하고 실행 전에 다시 컴파일하세요. 컴파일에 실패해도 이전 실행 파일은 남아 있을 수 있습니다.
모든 구매를 보관하고 출력하기
이 단계에서는 구매를 별도 레코드로 보관하고 구매 세 행이 있는 완전한 영수증을 만듭니다.
Workspace의 /home/labex/project/receipts에서 시작하세요. 카탈로그와 미완성 프로그램을 읽습니다.
cd /home/labex/project/receipts
cat catalog.h
cat till.c
헤더 파일 catalog.h는 판매대 이름, 최대 구매 여섯 건, 최대 수량 20, 네 카탈로그 항목을 제공합니다. #include "catalog.h"는 컴파일할 때 이 정의들을 포함합니다. #define은 상수나 초기화 목록에 이름을 붙이고, CATALOG_ROWS는 상품 배열을 초기화할 행들을 제공합니다. 카탈로그 정의의 행 끝에 있는 역슬래시(\)는 같은 #define을 다음 물리적 행으로 이어 줍니다. 이 실습에서 catalog.h, parse.h, driver.h, read-line.h는 변경하지 마세요.
구조체는 관련 필드를 하나의 레코드로 묶습니다. Product는 정수 id, 이름용 32문자 배열, 정수 가격 cents를 묶습니다. typedef를 사용하면 이 레코드 타입에 짧은 이름 Product를 쓸 수 있습니다. C 문자열은 값이 0인 문자 \0으로 끝나므로 이름 배열에는 보이는 문자 외에 종료 문자 공간도 필요합니다. 제공된 이름은 이 공간에 들어가며, 임의의 이름을 파싱하는 과제는 아닙니다.
Purchase는 두 번째 레코드 타입입니다. 배열에서 상품의 위치와 해당 구매의 수량을 보관합니다. basket[line_count].quantity에서 대괄호는 구매를 선택하고 점은 그 수량 필드를 선택합니다. 위치는 0부터 시작합니다. 전역 static 배열과 line_count는 0으로 초기화됩니다. sizeof products / sizeof products[0]은 배열에 완전한 상품 레코드가 몇 개 있는지 계산합니다.
제공된 find_product 반복문은 상품 ID로 검색하여 배열 위치를 반환하고, 상품이 없으면 -1을 반환합니다. Water라는 두 상품은 ID와 가격이 다릅니다. 103은 75센트이고 104는 무료입니다. 이름만으로 상품을 식별할 수 없습니다. 카탈로그에는 네 상품이 있고 ID는 1999, 가격은 09999센트입니다. 이 범위의 다른 네 행짜리 카탈로그로 다시 컴파일해도 같은 프로그램이 작동해야 합니다.
배열에 쓰기 전에 ID, 수량, 남은 용량을 검사하여 approve_purchase를 완성하세요. 그 후에만 상품 위치와 수량을 basket[line_count]에 저장하고 카운터를 증가시킵니다. 수량은 1~20이며 가격 0은 유효합니다. 반복된 ID를 합치지 마세요. 나중에 구매한 것은 다른 행입니다. 허용된 최대 가격과 수량으로 여섯 번 구매해도 최대 6 × 20 × 9999 = 1,199,880센트이므로 부호 있는 32비트 정수에 들어갑니다.
puts는 끝이 표시된 문자열을 출력하고 줄바꿈을 추가합니다. printf는 NUL 종료 문자열용 %s, 정수용 %d 같은 서식 지정자를 사용합니다. print_receipt는 승인된 모든 구매를 순회합니다. Product product = ...는 해당 카탈로그 레코드를 지역 변수에 복사합니다. 각 소계를 정수로 계산하고 합계에 더한 다음, 제목과 열 머리글 뒤에서 유일한 마지막 TOTAL 앞까지 완전한 행을 순서대로 출력하세요. 세로 막대로 나뉜 다섯 필드를 사용합니다. 필드 주변 공백과 빈 행은 달라도 되지만, 추가 레코드나 빠진 필드가 있으면 완전한 영수증이 아닙니다.
다음 완성 코드로 till.c를 교체하세요. cat > till.c는 파일을 교체하고 <<'EOF'는 here-document를 시작합니다. 따옴표로 묶인 구분자는 EOF만 있는 행까지 C 텍스트를 그대로 복사합니다. 마지막 행까지 전체 블록을 복사하세요.
cat > till.c <<'EOF'
#include <stdio.h>
#include "catalog.h"
#include "parse.h"
typedef struct {
int id;
char name[32];
int cents;
} Product;
typedef struct {
int product_index;
int quantity;
} Purchase;
static const Product products[] = CATALOG_ROWS;
static const int product_count = sizeof products / sizeof products[0];
static Purchase basket[MAX_LINES];
static int line_count;
static int find_product(int id) {
for (int i = 0; i < product_count; i++)
if (products[i].id == id) return i;
return -1;
}
static int approve_purchase(int id, int quantity) {
int index = find_product(id);
if (index < 0 || quantity < 1 || quantity > MAX_QUANTITY || line_count >= MAX_LINES)
return 0;
basket[line_count].product_index = index;
basket[line_count].quantity = quantity;
line_count++;
return 1;
}
static void print_receipt(void) {
int total = 0;
puts(SHOP_TITLE);
puts("ID | ITEM | QTY | UNIT | SUBTOTAL");
for (int i = 0; i < line_count; i++) {
Product product = products[basket[i].product_index];
int quantity = basket[i].quantity;
int subtotal = product.cents * quantity;
total += subtotal;
printf("%d | %s | %d | %d.%02d | %d.%02d\n",
product.id, product.name, quantity,
product.cents / 100, product.cents % 100,
subtotal / 100, subtotal % 100);
}
printf("TOTAL | %d.%02d\n", total / 100, total % 100);
}
#include "driver.h"
EOF
parse.h와 driver.h는 이미 작동하는 입력 반복문을 제공합니다. 드라이버는 ID와 수량을 묻고 승인 함수와 출력 함수를 호출합니다. 기존 이름과 시그니처는 연결 지점이므로 표시된 대로 유지하세요. 내부 레코드 타입 이름과 지역 변수 이름 자체는 평가 대상이 아닙니다. 다음 실습에서 입력 파싱의 경계를 배웁니다.
완전한 프로그램을 컴파일하세요.
gcc -std=c11 -Wall -Wextra till.c -o till
드라이버는 프롬프트와 메시지를 표준 오류로 보내므로 터미널에 계속 표시됩니다. 완성된 영수증만 표준 출력으로 갑니다. 새 출력 파일을 사용해 실행하세요.
./till > receipt.txt
프로그램은 입력을 기다립니다. 표의 각 값을 해당 프롬프트에 입력하고 매번 Enter를 누르세요. 셸 명령으로 입력하면 안 됩니다. done은 소문자로 정확히 일치해야 하는 완료 명령이며 Product ID 프롬프트에서만 작동합니다.
| Product ID 입력 | Quantity 입력 | 결과 |
|---|---|---|
101 |
2 |
Added line 1 |
102 |
1 |
Added line 2 |
101 |
1 |
Added line 3, 이전 101 구매도 유지 |
done |
수량을 묻지 않음 | 영수증을 완료하고 셸로 돌아감 |
프롬프트와 Added line 메시지가 계속 보이는 이유는 드라이버가 별도 스트림인 표준 오류에 쓰기 때문입니다. 완전한 영수증만 표준 출력으로 가고 >가 이를 receipt.txt로 보냅니다. 셸 프롬프트가 돌아오면 실제로 저장된 영수증을 읽으세요.
cat receipt.txt
예상 내용은 다음과 같습니다.
SUNNY SNACKS
ID | ITEM | QTY | UNIT | SUBTOTAL
101 | Oat bar | 2 | 1.25 | 2.50
102 | Fruit cup | 1 | 2.30 | 2.30
101 | Oat bar | 1 | 1.25 | 1.25
TOTAL | 6.05
Oat bar의 두 구매는 따로 유지됩니다. Fruit cup을 포함한 소계를 더하면 250 + 230 + 125 = 605센트입니다. 이 행들이 없는 합계는 겉보기에 맞더라도 고객의 거래 세부 정보를 잃게 됩니다.
숫자는 십진 숫자를 사용하며 선택적인 부호와 앞뒤의 ASCII 공백 또는 탭을 허용합니다. 앞자리 0도 허용됩니다. ID는 카탈로그에 있어야 하고 수량은 1..20이며 각 행은 Enter 앞까지 최대 63바이트여야 합니다. ID나 수량을 거부하면 대기 중인 후보를 버리고 승인된 구매를 바꾸지 않은 채 Product ID로 돌아갑니다. 제공된 리더는 너무 긴 행을 소비하여 나중 답변으로 나누지 않습니다. 승인할 수 있는 구매는 최대 여섯 건입니다. 빈 장바구니에서 done을 입력하면 합계 0.00의 유효한 빈 영수증을 만듭니다.
완료 대신 취소하려면 빈 입력 프롬프트에서 Ctrl+D를 누르세요. 이 파일 끝 신호 또는 미완성 행 도중의 EOF는 상태 2로 취소하고 완전한 영수증을 출력하지 않습니다. 미완료 거래를 내보내거나 보존하지 않습니다. 리디렉션은 이미 출력 파일을 열었으므로 의도적인 취소를 시험할 때 다른 이름을 사용하세요. 수량으로 입력한 done은 잘못된 수량 텍스트이지 완료가 아닙니다.
영수증에 마지막 구매만 있다면 basket[line_count]에 저장하고 승인할 때마다 카운터를 늘리는지 확인하세요. 저장하고 다시 컴파일한 뒤, 같은 필수 구매를 입력하는 새 거래를 실행합니다. 실행마다 빈 장바구니로 시작하며 이전 영수증 텍스트를 다시 읽지 않습니다. till.c, 실행 파일 till, 제공 파일, 완전한 receipt.txt를 보관하세요. 코스 검사에서는 일회용 디렉터리에서 수정하지 않은 코드 복사본을 실행 시 경계 검사와 함께 실행하기도 합니다. 해당 입력으로 실행되는 오류를 감지할 수 있지만, 완전한 메모리 안전성 증명은 아니며 파일을 고치지도 않습니다.
요약
상품 한 개의 예제를 여러 구매 행이 있는 완전한 영수증으로 확장했습니다. 구조체, 개별 구매의 배열, 안정적인 카탈로그 ID, 정수 센트 연산을 사용하고 실제 결과를 저장하여 읽었습니다. 다음 실습에서는 구매를 이 배열에 넣기 전에 입력을 검증하는 경계를 다룹니다.


