Putting It Together: A Menu-Driven Program
Chapter Forty-Four
Syllabus topic 4, "User-defined data types- structure and union"
Pages 216 to 222 of 222
In one line
A menu-driven program is a do-while loop round a switch, with the data in an array of structures and each menu item in a function of its own.
What this chapter is for
Every technique in this book has been shown on its own. A program is not a collection of techniques; it is a set of decisions about how they fit together. This chapter makes those decisions explicitly, because that is what a mini project is being marked on.
The five decisions, and the chapter that each one comes from:
| Decision | Chapter |
|---|---|
| The data is an array of structures with a count | 42, 36 |
| Each menu item is one function with one job | 33 |
| Functions that change the data take a pointer | 40 |
The menu is a do-while round a switch | 29, 26 |
| Every input is read and checked in one place | 15, 29 |
The design, before the code
The record. An account has a number, a holder's name and a balance. That is a structure.
The store. A fixed array of them and a count of how many are in use. MAX_ACCOUNTS is the capacity; count is the population.
Finding an account. One function, returning the index or -1. Chapter 30's search idiom. Everything else uses it, so the search is written once.
The operations. Open, deposit, withdraw, display one, display all. Five functions, each doing one thing and printing its own report.
Who owns the data. main owns the array. Every function that changes it takes a pointer to it; every function that only reads takes a const pointer. That is chapter 40, and it means you can tell from the parameter list which operations can alter the bank.
Writing those five paragraphs before any code is the single most useful habit this book can leave you with. Every one of them is a sentence a viva examiner will ask you to say out loud.
The program
#include <stdio.h>
#include <string.h>
#define MAX_ACCOUNTS 50
#define NAME_LEN 30
struct account {
int number;
char name[NAME_LEN];
double balance;
};
struct bank {
struct account accounts[MAX_ACCOUNTS];
int count;
int next_number;
};
/* ---- input, read and checked in ONE place ---- */
/* Reads one whole line and discards it, so a bad entry cannot be read twice. */
static void discard_line(void)
{
int c;
while ((c = getchar()) != '\n' && c != EOF) {
/* nothing: the line is being thrown away */
}
}
static int read_int(const char *prompt, int *out)
{
printf("%s", prompt);
if (scanf("%d", out) != 1) {
discard_line();
printf(" that was not a whole number\n");
return 0;
}
discard_line();
return 1;
}
static int read_double(const char *prompt, double *out)
{
printf("%s", prompt);
if (scanf("%lf", out) != 1) {
discard_line();
printf(" that was not an amount\n");
return 0;
}
discard_line();
return 1;
}
static int read_name(const char *prompt, char *out, int size)
{
printf("%s", prompt);
if (fgets(out, size, stdin) == NULL) {
return 0;
}
out[strcspn(out, "\n")] = '\0';
return out[0] != '\0';
}
/* ---- the bank ---- */
static void bank_init(struct bank *b)
{
b->count = 0;
b->next_number = 1001;
}
/* Chapter 30's search idiom: the index, or -1. */
static int find_account(const struct bank *b, int number)
{
for (int i = 0; i < b->count; i++) {
if (b->accounts[i].number == number) {
return i;
}
}
return -1;
}
static void print_one(const struct account *a)
{
printf(" %-6d %-30s %12.2f\n", a->number, a->name, a->balance);
}
static void open_account(struct bank *b)
{
char name[NAME_LEN];
double opening;
if (b->count >= MAX_ACCOUNTS) {
printf(" the bank is full: %d accounts is the limit\n", MAX_ACCOUNTS);
return;
}
if (!read_name(" Holder's name: ", name, (int) sizeof name)) {
printf(" a name is needed\n");
return;
}
if (!read_double(" Opening balance: ", &opening)) {
return;
}
if (opening < 0) {
printf(" an opening balance cannot be negative\n");
return;
}
struct account *a = &b->accounts[b->count];
a->number = b->next_number;
strncpy(a->name, name, sizeof a->name - 1);
a->name[sizeof a->name - 1] = '\0';
a->balance = opening;
b->count++;
b->next_number++;
printf(" opened account %d for %s with %.2f\n",
a->number, a->name, a->balance);
}
static void deposit(struct bank *b)
{
int number;
double amount;
if (!read_int(" Account number: ", &number)) { return; }
int at = find_account(b, number);
if (at < 0) {
printf(" there is no account %d\n", number);
return;
}
if (!read_double(" Amount to deposit: ", &amount)) { return; }
if (amount <= 0) {
printf(" a deposit must be more than zero\n");
return;
}
b->accounts[at].balance += amount;
printf(" deposited %.2f; account %d now holds %.2f\n",
amount, number, b->accounts[at].balance);
}
static void withdraw(struct bank *b)
{
int number;
double amount;
if (!read_int(" Account number: ", &number)) { return; }
int at = find_account(b, number);
if (at < 0) {
printf(" there is no account %d\n", number);
return;
}
if (!read_double(" Amount to withdraw: ", &amount)) { return; }
if (amount <= 0) {
printf(" a withdrawal must be more than zero\n");
return;
}
if (amount > b->accounts[at].balance) {
printf(" account %d holds only %.2f, so %.2f cannot be withdrawn\n",
number, b->accounts[at].balance, amount);
return;
}
b->accounts[at].balance -= amount;
printf(" withdrew %.2f; account %d now holds %.2f\n",
amount, number, b->accounts[at].balance);
}
static void show_one(const struct bank *b)
{
int number;
if (!read_int(" Account number: ", &number)) { return; }
int at = find_account(b, number);
if (at < 0) {
printf(" there is no account %d\n", number);
return;
}
printf(" %-6s %-30s %12s\n", "Number", "Holder", "Balance");
print_one(&b->accounts[at]);
}
static void show_all(const struct bank *b)
{
if (b->count == 0) {
printf(" no accounts have been opened yet\n");
return;
}
double total = 0.0;
printf(" %-6s %-30s %12s\n", "Number", "Holder", "Balance");
for (int i = 0; i < b->count; i++) {
print_one(&b->accounts[i]);
total += b->accounts[i].balance;
}
printf(" %d account(s), holding %.2f in total\n", b->count, total);
}
static void menu(void)
{
printf("\n--- Bank management ---\n");
printf("1 open an account\n");
printf("2 deposit\n");
printf("3 withdraw\n");
printf("4 show one account\n");
printf("5 show all accounts\n");
printf("0 quit\n");
}
int main(void)
{
struct bank b;
int choice;
bank_init(&b);
do {
menu();
if (!read_int("Choice: ", &choice)) {
choice = -1;
continue;
}
switch (choice) {
case 1: open_account(&b); break;
case 2: deposit(&b); break;
case 3: withdraw(&b); break;
case 4: show_one(&b); break;
case 5: show_all(&b); break;
case 0: printf(" closing. Goodbye.\n"); break;
default: printf(" %d is not on the menu\n", choice); break;
}
} while (choice != 0);
return 0;
}Putting It Together: A Menu-Driven Program
And a session that exercises every branch, including the ones that must refuse:
Putting It Together: A Menu-Driven Program
1
Anita Desai
5000
1
Rahul Mehta
2500
5
2
1001
1500
3
1002
9000
3
1002
500
4
1001
2
9999
6
0
--- Bank management ---
1 open an account
2 deposit
3 withdraw
4 show one account
5 show all accounts
0 quit
Choice: Holder's name: Opening balance: opened account 1001 for Anita Desai with 5000.00
--- Bank management ---
1 open an account
2 deposit
3 withdraw
4 show one account
5 show all accounts
0 quit
Choice: Holder's name: Opening balance: opened account 1002 for Rahul Mehta with 2500.00
--- Bank management ---
1 open an account
2 deposit
3 withdraw
4 show one account
5 show all accounts
0 quit
Choice: Number Holder Balance
1001 Anita Desai 5000.00
1002 Rahul Mehta 2500.00
2 account(s), holding 7500.00 in total
--- Bank management ---
1 open an account
2 deposit
3 withdraw
4 show one account
5 show all accounts
0 quit
Choice: Account number: Amount to deposit: deposited 1500.00; account 1001 now holds 6500.00
--- Bank management ---
1 open an account
2 deposit
3 withdraw
4 show one account
5 show all accounts
0 quit
Choice: Account number: Amount to withdraw: account 1002 holds only 2500.00, so 9000.00 cannot be withdrawn
--- Bank management ---
1 open an account
2 deposit
3 withdraw
4 show one account
5 show all accounts
0 quit
Choice: Account number: Amount to withdraw: withdrew 500.00; account 1002 now holds 2000.00
--- Bank management ---
1 open an account
2 deposit
3 withdraw
4 show one account
5 show all accounts
0 quit
Choice: Account number: Number Holder Balance
1001 Anita Desai 6500.00
--- Bank management ---
1 open an account
2 deposit
3 withdraw
4 show one account
5 show all accounts
0 quit
Choice: Account number: there is no account 9999
--- Bank management ---
1 open an account
2 deposit
3 withdraw
4 show one account
5 show all accounts
0 quit
Choice: 6 is not on the menu
--- Bank management ---
1 open an account
2 deposit
3 withdraw
4 show one account
5 show all accounts
0 quit
Choice: closing. Goodbye.Putting It Together: A Menu-Driven Program
Read the output against the program
Walk the session and notice what the program refused to do, because that is what it is being marked on.
- Two accounts opened, numbered automatically from 1001. The user never chooses a number, so two accounts cannot collide.
- Show all printed both and totalled them.
- A deposit of 1500 into 1001 succeeded and reported the new balance.
- A withdrawal of 9000 from 1002 was refused, because the account held 2500, and the balance was left alone.
- A withdrawal of 500 from 1002 succeeded.
- A deposit into account 9999 was refused, because there is no such account, and it was refused before the amount was asked for. The session therefore never supplies an amount for it, which is exactly what happens at a real keyboard: the user is returned to the menu without having typed one.
- Menu choice 6 was reported and ignored.
- 0 ended the loop.
That last-but-one point is a design decision worth stating in a viva: deposit finds the account first and asks for the amount second. Asking for the amount and then discovering the account does not exist wastes the user's time and leaves a number in the input that the next read would have to deal with.
The parts of the program that are not about banking
Three pieces of this program are general and you should take them to every menu-driven program you write.
1. discard_line. After a failed scanf, the text that failed is still in the input. Without throwing the rest of the line away, the next read fails on the same text, and the menu spins. This is the single commonest reason a student's menu program hangs in a practical examination.
static void discard_line(void)
{
int c;
while ((c = getchar()) != '\n' && c != EOF) { }
}The c != EOF half is what stops it spinning for ever when the input has ended.
2. One reader per type, returning success. read_int, read_double and read_name each prompt, read, check and report. Every caller is then one line: if (!read_int(...)) { return; }. Without them, the same six lines of checking appear in five functions.
3. const on the reading operations. show_one and show_all take const struct bank *. The compiler will refuse a write through them, so a reader of the code knows those two cannot alter the bank. Chapter 40.
Putting It Together: A Menu-Driven Program
What this program does not do, and what you would add next
Being honest about the limits is part of presenting a mini project.
- Nothing is saved. Close the program and the accounts are gone. Files are the next chapter of your degree, not of this book, and
fopen,fprintfandfscanfare what you would add. - The capacity is fixed at
MAX_ACCOUNTS. Memory obtained while the program runs, withmalloc, is the general answer and is a later semester's topic. - An account cannot be closed. Adding it means either moving the later elements down or marking the record as unused, and the second is usually better.
- Money is a
double. Real banking systems hold amounts in the smallest unit as an integer, in paise, because adoublecannot represent 0.10 exactly and repeated arithmetic accumulates error. Chapter 9 gave the reason. For a first-semester mini project adoubleis expected; knowing why it is wrong is what a viva rewards.
An examiner who asks "what would you improve" is inviting exactly that list. Having it ready is worth more than any extra feature.
Where each technique in this book appears
| Technique | Chapter | Where in the program |
|---|---|---|
| Structure | 42 | struct account, struct bank |
| Nested structure | 42 | struct bank contains an array of struct account |
| Array of structures | 36, 42 | b->accounts |
| Pointer to a structure | 39, 42 | every function's struct bank *b |
const pointer parameter | 40 | show_one, show_all, find_account |
-> | 42 | b->count, a->balance |
| Function returning an index or -1 | 30, 33 | find_account |
do-while | 29 | the menu loop |
switch with default | 26 | dispatching the choice |
for loop over an array | 28, 36 | show_all, find_account |
while loop to end of line | 27 | discard_line |
scanf return value checked | 15 | read_int, read_double |
fgets and strcspn | 12, 37 | read_name |
strncpy with a hand-written terminator | 37 | open_account |
| Compound assignment | 18 | balance += amount |
| Named constants | 10, 22 | MAX_ACCOUNTS, NAME_LEN |
| Formatted output widths | 15 | every printf with %-6d and %12.2f |
That table is the answer to "explain your project". Forty-four chapters, one program.
What this does NOT mean
A mini project is not a long main. It is a set of small functions with one main that dispatches.
A menu is not a while loop. It must be shown at least once, so it is a do-while.
Validation is not optional. Most of the marks in a practical examination are lost to programs that work on correct input and misbehave on anything else.
Putting It Together: A Menu-Driven Program
break in a case does not end the menu. It ends the switch. The loop ends because choice is 0, which its condition tests. Chapter 30.
A fixed-size array is not a defect in a first project. It is a documented limit. Saying what the limit is and what you would do instead is better than pretending otherwise.
Quick revision
- A menu-driven program is a
do-whileround aswitch, with 0 to quit. - Data in a structure; many records in an array of them with a separate count.
- One function per menu item, each with one job.
- A function that changes the data takes a pointer; one that only reads takes a
constpointer. - Search once, in one function, returning the index or -1.
- Read and check every input in one place, with one small reader per type returning success.
- After a failed
scanf, discard the rest of the line, or the menu spins for ever. - Find the record before asking for the amount.
- Refuse an overdraft, a non-positive amount and an unknown account, and say so.
- Know the program's limits: nothing is saved, the capacity is fixed, and money in a
doubleis not exact.
Test yourself
1. Why is the menu loop a do-while rather than a while?
Because the menu must be shown at least once before the user can make any choice.
2. Why does a failed scanf need the rest of the line thrown away?
Because the characters that failed to convert are still in the input, so the next read fails on the same text and the menu repeats for ever.
3. Which functions in the program cannot change the bank, and how can you tell?
find_account, show_one and show_all, because their parameter is const struct bank * and the compiler will refuse any write through it.
4. Why does deposit look up the account before asking for the amount?
So that an unknown account number is reported at once, without asking for an amount that will not be used.
5. What is find_account returning when it returns -1, and why -1?
That there is no account with that number. -1 is used because it cannot be a valid array index, so the caller can test for it unambiguously.
6. Why should a real banking program not hold money in a double?
Because values such as 0.10 have no exact binary representation, so repeated arithmetic accumulates error. Amounts are held as whole numbers of the smallest unit, in paise, in an integer type.
7. How would you add the ability to close an account?
Either move every later element of the array down one and decrement the count, or add a member marking the record as unused and skip it everywhere. The second is simpler and does not disturb the other records.
Putting It Together: A Menu-Driven Program
What can be asked on this, and how to answer it
"Create a mini project on a bank management system. The program should be menu driven." Give this chapter's program, or a program of the same shape. Before writing code, say the five design decisions out loud: array of structures with a count, one function per operation, pointers for the operations that change the data, do-while round a switch, and input checked in one place. That is what the marks are for.
"Explain the structure of your project." Use the last table: name each technique and say where in the program it appears. An answer that walks the program top to bottom without naming the techniques is worth much less.
"What happens if the user types a letter where a number is expected?" scanf returns 0, the reader function reports it, the offending line is discarded and the menu is shown again. Say that without discarding the line the program would loop for ever, because that is the part examiners test.
"What are the limitations of your project and how would you improve it?" Nothing is saved between runs, so add file handling; the capacity is fixed, so allocate memory as needed; accounts cannot be closed; and money should be held in an integer number of paise rather than a double.
"Why did you use a structure rather than separate arrays?" Because the account number, the holder's name and the balance belong to one account, and a structure says so in the type. With separate arrays nothing keeps the three in step, and one sort or one deletion would separate them.
The rest of this subject
These notes are cut from the University's printed syllabus. Open the syllabus itself for the same subject.