はじめに
C言語によるファイル操作の学習は、単にデータを書き込んだり読み込んだりするだけでは終わりません。実用的なアプリケーションを開発するには、保存したデータの更新や削除といった、より高度な操作が不可欠です。
この記事では、基本的な連絡先の「追加」「読み込み」「検索」機能に加え、「更新」「削除」機能を実装した、本格的な連絡先管理プログラムの作成方法を解説します。いわゆるCRUD(Create, Read, Update, Delete)と呼ばれる、データ操作の基本要素をすべて網羅します。
入力処理にはfgets
を使用し、スペースを含む氏名や住所も扱えるように改良しています。ファイル操作の応用的なテクニックの学習に適した内容です。
1. プログラムの全体設計と構造体
プログラムは、以下の5つの主要機能を持ちます。
- Create(追加): 新しい連絡先をファイルに保存する。
- Read(読み込み・検索): 番号や名前で連絡先を検索し、表示する。
- Update(更新): 既存の連絡先情報を新しい内容で上書きする。
- Delete(削除): 既存の連絡先をファイルから削除する。
- List(一覧表示): 保存されているすべての連絡先を表示する。
データを管理するための構造体は、前回と同様のものを使用します。
// 連絡先一件分のデータを格納する構造体
typedef struct {
char name[60];
char address[120];
} Contact;
2. 機能の実装
各機能を個別の関数として実装します。特に、今回の中心となる「更新」と「削除」のロジックに注目してください。
2-1. ユーティリティ関数
まず、コード全体で利用する補助的な関数を準備します。
- 入力バッファのクリア:
fgets
を使った安全な入力の後、バッファに残る不要な改行文字などをクリアします。 - 安全な文字列入力:
fgets
を使い、改行文字を取り除く処理をまとめた関数です。
#include <stdio.h>
#include <string.h>
#include <stdlib.h> // for exit, remove, rename
#define FILENAME "contacts_db.dat"
// 入力バッファをクリアする関数
void clearInputBuffer() {
int c;
while ((c = getchar()) != '\n' && c != EOF);
}
// 安全に文字列入力を受け取る関数
void getStringInput(char* buffer, int size) {
if (fgets(buffer, size, stdin) != NULL) {
// fgetsで読み込んだ末尾の改行文字をNULL文字に置き換える
size_t len = strlen(buffer);
if (len > 0 && buffer[len - 1] == '\n') {
buffer[len - 1] = '\0';
} else {
// バッファがいっぱいになるまで入力された場合、残りの入力をクリア
clearInputBuffer();
}
}
}
2-2. 連絡先の一覧表示 (listContacts
)
ファイルに保存されている全ての連絡先を、先頭から順に読み込んで表示します。
// 連絡先を一覧表示する関数
void listContacts() {
FILE* fp;
Contact person;
int index = 1;
if (fopen_s(&fp, FILENAME, "rb") != 0) {
fprintf(stderr, "エラー: データベースファイルを開けません。\n");
return;
}
printf("\n--- 連絡先一覧 ---\n");
while (fread(&person, sizeof(Contact), 1, fp) == 1) {
printf("[%d]\n", index++);
printf(" 名前: %s\n", person.name);
printf(" 住所: %s\n", person.address);
printf("--------------------\n");
}
if (index == 1) {
printf("連絡先は登録されていません。\n");
}
fclose(fp);
}
2-3. 連絡先の追加 (addContact
)
新しい連絡先情報をユーザーから受け取り、ファイルの末尾に追記します。
// 連絡先を追加する関数
void addContact() {
FILE* fp;
Contact person;
if (fopen_s(&fp, FILENAME, "ab") != 0) {
fprintf(stderr, "エラー: データベースファイルを開けません。\n");
return;
}
printf("\n--- 新しい連絡先の追加 ---\n");
printf("名前を入力してください: ");
getStringInput(person.name, sizeof(person.name));
printf("住所を入力してください: ");
getStringInput(person.address, sizeof(person.address));
fwrite(&person, sizeof(Contact), 1, fp);
fclose(fp);
printf("連絡先を正常に追加しました。\n");
}
2-4. 連絡先の更新 (updateContact
)
ここからが新しい機能です。更新は、以下の手順で行います。
- 更新対象の連絡先を一覧表示し、ユーザーに番号で選択させる。
- 指定された番号のデータを読み込む。
- ファイルポインタを
fseek
でそのデータの開始位置に戻す。 - 新しいデータを入力させ、
fwrite
で元のデータに上書きする。
// 連絡先を更新する関数
void updateContact() {
FILE* fp;
Contact person;
int targetIndex, currentIndex = 0;
long filePosition;
listContacts();
printf("\n更新したい連絡先の番号を入力してください (0でキャンセル): ");
scanf_s("%d", &targetIndex);
clearInputBuffer();
if (targetIndex <= 0) return;
if (fopen_s(&fp, "r+b") != 0) { // 読み書き両用でバイナリオープン
fprintf(stderr, "エラー: データベースファイルを開けません。\n");
return;
}
while (fread(&person, sizeof(Contact), 1, fp) == 1) {
currentIndex++;
if (currentIndex == targetIndex) {
// 更新対象が見つかった
printf("\n--- 連絡先の更新 ---\n");
printf("現在の名前: %s\n", person.name);
printf("新しい名前を入力してください: ");
getStringInput(person.name, sizeof(person.name));
printf("現在の住所: %s\n", person.address);
printf("新しい住所を入力してください: ");
getStringInput(person.address, sizeof(person.address));
// ファイルポインタをこのレコードの開始位置に戻す
filePosition = (long)(currentIndex - 1) * sizeof(Contact);
fseek(fp, filePosition, SEEK_SET);
// 更新されたデータを書き込む
fwrite(&person, sizeof(Contact), 1, fp);
printf("連絡先を更新しました。\n");
fclose(fp);
return;
}
}
printf("指定された番号の連絡先は見つかりませんでした。\n");
fclose(fp);
}
2-5. 連絡先の削除 (deleteContact
)
ファイルの途中からデータを物理的に削除するのは複雑なため、以下の安全な手順を踏みます。
- 削除対象以外のデータを、一時的な新しいファイル(
temp.dat
)に書き出す。 - 元のファイルを削除する (
remove
)。 - 一時ファイルを元のファイル名に変更する (
rename
)。
これにより、結果的に対象データが削除されたのと同じ状態になります。
// 連絡先を削除する関数
void deleteContact() {
FILE* fp, * tempFp;
Contact person;
int targetIndex, currentIndex = 1;
int found = 0;
const char* tempFilename = "temp.dat";
listContacts();
printf("\n削除したい連絡先の番号を入力してください (0でキャンセル): ");
scanf_s("%d", &targetIndex);
clearInputBuffer();
if (targetIndex <= 0) return;
if (fopen_s(&fp, FILENAME, "rb") != 0) {
fprintf(stderr, "エラー: 元ファイルを開けません。\n");
return;
}
if (fopen_s(&tempFp, tempFilename, "wb") != 0) {
fprintf(stderr, "エラー: 一時ファイルを作成できません。\n");
fclose(fp);
return;
}
// 元のファイルから1件ずつ読み込み、削除対象でなければ一時ファイルに書き込む
while (fread(&person, sizeof(Contact), 1, fp) == 1) {
if (currentIndex == targetIndex) {
found = 1; // 削除対象なので何もしない
} else {
fwrite(&person, sizeof(Contact), 1, tempFp);
}
currentIndex++;
}
fclose(fp);
fclose(tempFp);
if (found) {
// 元のファイルを削除し、一時ファイルをリネームする
if (remove(FILENAME) != 0) {
fprintf(stderr, "エラー: 元ファイルの削除に失敗しました。\n");
} else if (rename(tempFilename, FILENAME) != 0) {
fprintf(stderr, "エラー: ファイルのリネームに失敗しました。\n");
} else {
printf("連絡先を正常に削除しました。\n");
}
} else {
printf("指定された番号の連絡先は見つかりませんでした。\n");
remove(tempFilename); // 不要な一時ファイルを削除
}
}
3. すべてを統合する main
関数
最後に、これらの機能をユーザーが選択できるメニューを持つmain
関数を作成します。
int main(void) {
int choice;
while (1) {
printf("\n===== 連絡先管理システム =====\n");
printf("1. 連絡先を一覧表示\n");
printf("2. 連絡先を追加\n");
printf("3. 連絡先を更新\n");
printf("4. 連絡先を削除\n");
printf("0. 終了\n");
printf("============================\n");
printf("操作を選択してください: ");
if (scanf_s("%d", &choice) != 1) {
fprintf(stderr, "不正な入力です。数値を入力してください。\n");
clearInputBuffer();
continue;
}
clearInputBuffer(); // scanfの後の改行文字をクリア
switch (choice) {
case 1: listContacts(); break;
case 2: addContact(); break;
case 3: updateContact(); break;
case 4: deleteContact(); break;
case 0:
printf("プログラムを終了します。\n");
return 0;
default:
printf("無効な選択です。もう一度入力してください。\n");
break;
}
}
return 0;
}
4. 全体のサンプルコード
以下に、全ての機能を統合した完全なソースコードを掲載します。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define FILENAME "contacts_db.dat"
// 構造体定義
typedef struct {
char name[60];
char address[120];
} Contact;
// プロトタイプ宣言
void clearInputBuffer();
void getStringInput(char* buffer, int size);
void listContacts();
void addContact();
void updateContact();
void deleteContact();
int main(void) {
int choice;
while (1) {
printf("\n===== 連絡先管理システム =====\n");
printf("1. 連絡先を一覧表示\n");
printf("2. 連絡先を追加\n");
printf("3. 連絡先を更新\n");
printf("4. 連絡先を削除\n");
printf("0. 終了\n");
printf("============================\n");
printf("操作を選択してください: ");
if (scanf_s("%d", &choice) != 1) {
fprintf(stderr, "不正な入力です。数値を入力してください。\n");
clearInputBuffer();
continue;
}
clearInputBuffer(); // scanfの後の改行文字をクリア
switch (choice) {
case 1: listContacts(); break;
case 2: addContact(); break;
case 3: updateContact(); break;
case 4: deleteContact(); break;
case 0:
printf("プログラムを終了します。\n");
return 0;
default:
printf("無効な選択です。もう一度入力してください。\n");
break;
}
}
return 0;
}
void clearInputBuffer() {
int c;
while ((c = getchar()) != '\n' && c != EOF);
}
void getStringInput(char* buffer, int size) {
if (fgets(buffer, size, stdin) != NULL) {
size_t len = strlen(buffer);
if (len > 0 && buffer[len - 1] == '\n') {
buffer[len - 1] = '\0';
} else {
clearInputBuffer();
}
}
}
void listContacts() {
FILE* fp;
Contact person;
int index = 1;
if (fopen_s(&fp, FILENAME, "rb") != 0) {
// ファイルが存在しない場合はエラーではなく、まだ登録がないことを示す
printf("\n連絡先はまだ登録されていません。\n");
return;
}
printf("\n--- 連絡先一覧 ---\n");
while (fread(&person, sizeof(Contact), 1, fp) == 1) {
printf("[%d]\n", index++);
printf(" 名前: %s\n", person.name);
printf(" 住所: %s\n", person.address);
printf("--------------------\n");
}
if (index == 1) {
printf("連絡先は登録されていません。\n");
}
fclose(fp);
}
void addContact() {
FILE* fp;
Contact person;
if (fopen_s(&fp, FILENAME, "ab") != 0) {
fprintf(stderr, "エラー: データベースファイルを開けません。\n");
return;
}
printf("\n--- 新しい連絡先の追加 ---\n");
printf("名前を入力してください: ");
getStringInput(person.name, sizeof(person.name));
printf("住所を入力してください: ");
getStringInput(person.address, sizeof(person.address));
fwrite(&person, sizeof(Contact), 1, fp);
fclose(fp);
printf("連絡先を正常に追加しました。\n");
}
void updateContact() {
FILE* fp;
Contact person;
int targetIndex, currentIndex = 0;
long filePosition;
listContacts();
printf("\n更新したい連絡先の番号を入力してください (0でキャンセル): ");
if (scanf_s("%d", &targetIndex) != 1) {
fprintf(stderr, "不正な入力です。\n");
clearInputBuffer();
return;
}
clearInputBuffer();
if (targetIndex <= 0) return;
if (fopen_s(&fp, "r+b") != 0) {
fprintf(stderr, "エラー: データベースファイルを開けません。\n");
return;
}
while (fread(&person, sizeof(Contact), 1, fp) == 1) {
currentIndex++;
if (currentIndex == targetIndex) {
printf("\n--- [%d]の連絡先を更新 ---\n", targetIndex);
printf("現在の名前: %s\n", person.name);
printf("新しい名前を入力してください: ");
getStringInput(person.name, sizeof(person.name));
printf("現在の住所: %s\n", person.address);
printf("新しい住所を入力してください: ");
getStringInput(person.address, sizeof(person.address));
filePosition = (long)(currentIndex - 1) * sizeof(Contact);
fseek(fp, filePosition, SEEK_SET);
fwrite(&person, sizeof(Contact), 1, fp);
printf("連絡先を更新しました。\n");
fclose(fp);
return;
}
}
printf("指定された番号の連絡先は見つかりませんでした。\n");
fclose(fp);
}
void deleteContact() {
FILE* fp, * tempFp;
Contact person;
int targetIndex, currentIndex = 1;
int found = 0;
const char* tempFilename = "temp_db.dat";
listContacts();
printf("\n削除したい連絡先の番号を入力してください (0でキャンセル): ");
if (scanf_s("%d", &targetIndex) != 1) {
fprintf(stderr, "不正な入力です。\n");
clearInputBuffer();
return;
}
clearInputBuffer();
if (targetIndex <= 0) return;
if (fopen_s(&fp, FILENAME, "rb") != 0) {
fprintf(stderr, "エラー: 元ファイルを開けません。\n");
return;
}
if (fopen_s(&tempFp, tempFilename, "wb") != 0) {
fprintf(stderr, "エラー: 一時ファイルを作成できません。\n");
fclose(fp);
return;
}
while (fread(&person, sizeof(Contact), 1, fp) == 1) {
if (currentIndex != targetIndex) {
fwrite(&person, sizeof(Contact), 1, tempFp);
} else {
found = 1;
}
currentIndex++;
}
fclose(fp);
fclose(tempFp);
if (found) {
if (remove(FILENAME) != 0) {
fprintf(stderr, "エラー: 元ファイルの削除に失敗しました。\n");
remove(tempFilename); // 一時ファイルも削除
} else if (rename(tempFilename, FILENAME) != 0) {
fprintf(stderr, "エラー: ファイルのリネームに失敗しました。\n");
} else {
printf("連絡先を正常に削除しました。\n");
}
} else {
printf("指定された番号の連絡先は見つかりませんでした。\n");
remove(tempFilename);
}
}
まとめ
本記事では、C言語のファイル操作を用いて、データの**CRUD(作成、読み取り、更新、削除)**を全て実装した連絡先管理プログラムを作成しました。
- 入力処理の改善:
scanf_s
の代わりにfgets
を用いることで、スペースを含む文字列も安全に扱えるようになりました。 - 更新処理:
r+b
モードでファイルを開き、fseek
で目的の場所へ移動してfwrite
で上書きする、というランダムアクセスの実践的な応用を学びました。 - 削除処理: 一時ファイルを利用して、安全かつ確実に特定のレコードを削除する手法を実装しました。これはファイル操作における定石の一つです。
このプログラムは、C言語による実践的なアプリケーション開発の第一歩となります。ここから更に、名前による検索機能の追加、データの並べ替え(ソート)機能、より詳細なエラーハンドリングなどを実装することで、さらに高機能なプログラムへと発展させることが可能です。
この記事が、C言語学習の助けとなれば幸いです。