アドレス帳3
序文
静的アドレス帳の実装について【C言語】簡易アドレス帳の実装
動的アドレス帳の実装について【C言語】簡易アドレス帳2と動的メモリ管理入門
前回は静的アドレス帳を変更して容量が固定されなくなったので、今回は最後の問題を解決します。
- プログラム終了後にデータを保存することはできず、プログラムに入るたびにデータを再記録する必要があります。
そこで今回はC言語のファイル操作の知識を借りて、アドレス帳のデータ継承の問題を実現してみます。
次に、この機能を実現するのに役立つ知識を最初に理解する必要があります。
ファイル操作
ファイル名
必要なファイルを操作できるようにするには、各ファイルの一意のファイル識別子、つまりファイル名を知る必要があります。
実際、次のメソッドの紹介では、ファイル名には 3 つの部分が含まれています文件路径
+文件名主干
+文件后缀
。これは、私たちが日常的に考えているファイル名とは異なります。これは、 パスが異なるファイル名を区別できるようにするためです。
実際、私たちが日常生活で参照するファイル名は 文件名主干
または 文件名主干
+文件后缀
ですが、実際 a>文件路径
もファイル名の一部としてカウントされます
ただし、: 一部のオペレーティング システムやプログラムでは、特殊なファイルやカスタム ファイルなど、ファイル拡張子の使用が不要または許可されない場合があります。 . 定義されたファイルタイプなど。したがって、シナリオによっては、ファイル名にファイル接尾辞が必ずしも含まれない場合があります。
ファイルの開閉
いくつかのファイルを手動で操作するときのプロセスを思い出してください。内部のコンテンツを操作するには、まずファイルを開いて、操作が完了したらファイルを閉じる必要がありますか?
したがって、プログラムでファイルを操作するには、まずプログラムにファイルを開かせ、最後にファイルを閉じる必要があります。
これら 2 つの関数は C 言語でも提供されています
打开文件
FILE* fopen (const char* filename, const char* mode );
ファイルが正常に開かれた場合は返します文件类型指针
(下記を参照)。それ以外の場合は null ポインタを返しますNULL
关闭文件
int fclose (FILE* stream);
ファイルが正常に閉じられた場合は戻ります0
、失敗した場合は戻りますEOF(-1)
その中に 1 つありますFILE*
。これはファイル操作用に C 言語で定義されたポインタ型で、**文件类型指针
* * と呼ばれます。一般に、これを と呼びます。文件指针
ファイルにアクセスすると、ファイルは文件信息区
と呼ばれるメモリ内スペースを維持します。この領域には、操作するファイルに関するさまざまな情報が保存されます。この領域は実際には構造体変数によって開かれており、この構造体の型は FILE
です。標準ライブラリのFILE
構造には、ファイルの場所ポインタやバッファ ポインタなどのファイル関連情報が含まれています。 、バッファサイズなど。特定の構造定義はオペレーティング システムとコンパイラによって異なる場合がありますが、通常はこれらの基本的なメンバー変数が含まれています。
ファイルを開くと、システムはファイルの状況に基づいてFILE
の構造変数を自動的に作成します。これは、ファイル情報領域を開くことを意味します。ファイルポインタはこの領域を指しており、プログラムもこの領域に格納されている情報を介してファイルにアクセスし、操作します。
文件的位置指针
と 文件类型指针
の違いに注意してください。どちらのポインタも 文件指针
と呼び出すことができますが、ファイル タイプ ポインタは と呼びます。はファイル情報領域を指し、ファイル位置ポインタはファイル内の情報を指します。これにより、対応する場所での情報の入出力が容易になります。
さらに、ほとんどの場合、ファイルの場所のポインターを操作している場合でも、実際に関数に渡されるのはファイルの種類のポインターであるため、多くの場合、それらを詳細に区別しません。< /span>< a i=1>ファイル ポインタは直接呼ばれます
残りのパラメータを順番に紹介します
filename
: ファイル名。操作対象のファイルがプロジェクト フォルダー内にある場合は、文件名主干
+文件后缀
と記述しますが、プロジェクト内にはありません。フォルダー。内部ファイルには完全なファイル名を使用する必要があります。ファイル パスを書き込むときは、エスケープ文字\
を使用してパスをオフセットできます。 . などの一部のエスケープ文字
FILE* pf = fopen("C:\\Users\\ShmentLife\\Desktop\\test\\test.txt", "r");
mode
: ファイルを開く方法。これは、ファイルに対するその後の操作に影響します。一般的な開き方法を次の表に示します。
ファイルの使い方 | 意味 | 指定したファイルが存在しない場合 |
---|---|---|
r (読み取り専用) |
データを入力するには、既存のテキスト ファイルを開きます | エラー |
w (書くだけ) |
データを出力するには、テキスト ファイルを開きます | 新しいファイルを作成する |
a (追加) |
テキストファイルの末尾にデータを追加します | 新しいファイルを作成する |
rb (読み取り専用) |
データを入力するには、バイナリ ファイルを開きます | エラー |
wb (書くだけ) |
データを出力するには、バイナリ ファイルを開きます | 新しいファイルを作成する |
ab (追加) |
バイナリファイルの末尾にデータを追加する | エラー |
r+ (読み書き) |
読み取りと書き込みの場合は、テキスト ファイルを開きます | エラー |
w+ (読み書き) |
読み取りと書き込みの場合は、新しいファイルを作成します | 新しいファイルを作成する |
a+ (読み書き) |
ファイルを開き、ファイルの最後で読み取りと書き込みを行う | 新しいファイルを作成する |
rb+ (読み書き) |
バイナリ ファイルを読み書き用に開きます。 | エラー |
wb+ (読み書き) |
読み取りと書き込みの場合は、新しいバイナリ ファイルを作成します | 新しいファイルを作成する |
ab+ (読み書き) |
バイナリ ファイルを開き、ファイルの最後で読み取りと書き込みを行う | 新しいファイルを作成する |
とりあえず、最初の 3 つについて理解しておきましょう。
最初の 3 つの意味について疑問を持つ人もいるかもしれませんが、「読む」「書く」「入力する」「出力する」とは誰を指すのでしょうか?
ファイル操作を実行するのは私たちが作成するプログラムであるため、読み取り、書き込み、入力、出力はすべてプログラムに関連しており、画像で表すことができます。
上記の知識を理解した上で、例を見てみましょう
int main() {
//打开文件
FILE* pf = fopen("test.txt", "w");
//判断成功与否,防止非法访问
if (pf == NULL) {
perror("fopen");
return 1;
}
//文件操作
//...
//关闭和置空指针
fclose(pf);
pf = NULL;
return 0;
}
同時に、"w"
"a"
の違いにも注意する必要があります。"w"
は毎回新しいファイルを作成しますが、"a"
以前のファイルに基づいて追加を続行します
どちらの場合もファイルを指定せずに新しいファイルを作成しますが、既存のファイルの処理方法は異なります。
次はファイルの操作方法を学んでいきます。
ファイルのシーケンシャルな読み取りと書き込み
C言語にはファイルの出力や入力を行うための関数がいくつか用意されていますが、まずはシーケンシャルな読み込みと書き込みの関数を紹介します。
シーケンシャル読み取りと書き込みとは何ですか? それは、読み取りと書き込みの順序を変更することはできず、前から後ろへのみ読み取ることができることを意味します。
以下に、順次読み取りおよび書き込み関数をいくつか示します。
関数 | 関数名 | 適用対象 |
---|---|---|
文字入力機能 | fgetc |
すべての入力ストリーム |
文字出力機能 | fputc |
すべての出力ストリーム |
テキスト行入力機能 | fgets |
すべての入力ストリーム |
テキスト行出力機能 | fputs |
すべての出力ストリーム |
フォーマット入力機能 | fscanf |
すべての入力ストリーム |
フォーマット出力機能 | fprintf |
すべての出力ストリーム |
バイナリ入力 | fread |
書類 |
バイナリ出力 | fwrite |
書類 |
ここで所有输出流
所有输入流
に何が当てはまるのか混乱する人もいるかもしれません。これにはフローの概念が関係します。
フローの概念
ストリームは実際にはプログラムと出力/入力端子の間の中継局に似ていますが、なぜこの中継局が必要なのでしょうか?
プログラムを書く場合、外部との入出力が必要になることがありますが、ストリームのような転送局がない場合、プログラマは「外部からどうやって読み込むか」を考慮してプログラムを書く必要があります。それを外の世界にエクスポートするにはどうすればよいですか?これに似た質問
ただし、ストリームの転送ステーションを使用すると、プログラマはそのような問題を心配しなくなります。プログラムを作成するときは、ストリームへの入出力のみを使用します。したがって、実際にはどのように入出力するのか (つまり、それらのデバイスやファイルをどのように処理するのか)ストリームから抜け出すには? データの読み取りと入出力) は、プログラムを作成するときに考慮すべき問題ではありません。
簡単に言えば、プログラムを作成するとき、ストリームは入出力データを抽象化し、プログラムを基盤となるデバイスの詳細から分離し、プログラムの汎用性と移植性を高めるのに役立ちます。たとえば、プログラムのコア ロジックを変更せずに、対応するストリーム操作を変更することで、プログラムを異なるオペレーティング システムやハードウェア プラットフォーム上で実行できます。
上で説明しましたFILE*
、これは実際にはストリームです。なぜでしょうか。ファイルの入出力を行う際、ファイルへの入出力を考慮せずにこのストリームだけを操作するため、 このストリームをファイルストリームと呼びます。
このストリームを使用する場合、このストリームのアクセス モード (読み取り専用、書き込み専用、追加など) を指定できます。同時に、さまざまな関数 (以下で説明する関数) を通じてこのストリームを操作できます。 ). ファイルの入出力の効果を実現するため
さらに、C 言語標準では 3 つの特殊なストリームstdin
(標準入力ストリーム) stdout
(標準出力ストリーム) < a i=3> (標準エラー出力ストリーム)、これら 3 つのストリームは標準ストリームとして定義され、プログラムの開始時に自動的に有効になります。したがって、上記のファイル ストリームのように事前に開いたり作成したりする必要はありませんstderr
実際、これらの標準ストリームはこれまでに何度も使用しています。たとえば、printf
を使用したとき、標準出力を実行すると前に言いましたが、実際には標準に出力されます。ストリームを読み取って画面に表示します (ただし、これは画面を通じて出力しなければならないという意味ではありませんが、通常は画面を通じて出力されます)。 scanf
同じことが当てはまります。キーボードでの入力は標準入力とみなされ、標準入力ストリームに配置されます。その後、scanf
は標準入力ストリームを読み取ります。データ< /span>
fgetc
fgetc
ファイルから文字を読み取るために使用されます。これは次のように定義されます。
int fgetc ( FILE * stream );
戻り値: 型はint
で、読み取った文字のASCIIコード値を返します。読み込みに失敗した場合、またはファイルの末尾を読み取った場合は、それが返されます。 EOF
パラメータの概要:stream
、ファイル ポインタ。ファイルを読み取るファイル情報領域を指します。
私たちのtest.txt
ファイルには次の情報が保存されていると仮定します
abcdef
次に、次のコードを通じて文字を読み取ることができます。
#include<stdio.h>
int main() {
//这里的文件打开方式要能够满足我们的使用方式
FILE* pf = fopen("test.txt", "r");
if (pf == NULL) {
perror("Error opening file");
return 0;
}
printf("%c", fgetc(pf));
fclose(pf);
pf = NULL;
return 0;
}
では、次の文字を読みたい場合、他の関数を使用せずにそれを行うことができるでしょうか? pf++
させれば十分ではないかと思う人もいるかもしれません。
しかし、これは絶対に不可能です。上で述べたように、pf
は FILE*
型として、ファイル情報を格納するファイル情報を指します。この時点で pf
タイプ サイズを 1 つスキップすると、不明なメモリ領域に到達し、不正アクセスが発生します。FILE
実際、次の文字にアクセスしたい場合は、fgetc
を複数回使用するだけで済みます。なぜなら、この関数を初めて使用するときは、 = 2>型内のファイル位置ポインタは自動的にファイル内の最初の文字を指し、この関数を使用するたびに、このポインタは自動的に 1 文字前にスキップしますFILE
ファイル内のすべての文字を読み取りたい場合は、fgetc
それを 6 回実行するだけです。
#include<stdio.h>
int main() {
FILE* pf = fopen("test.txt", "w");
if (pf == NULL) {
perror("Error opening file");
return 0;
}
for (int i = 0; i < 7; i++) {
printf("%c", fgetc(pf));
}
fclose(pf);
pf = NULL;
return 0;
}
この時点でループの数が 6 より大きい場合、fgetc
関数はファイルの最後まで読み取るときに EOF
に遭遇し、読み続けないでください
fputc
fputc
文字の出力とファイルへの文字の書き込みに使用されます。次のように定義されます。
int fputc ( int character, FILE * stream );
戻り値: 型はint
で、出力文字の ASCII コード値を返します。出力が失敗した場合は、 が返されます。EOF
パラメータの概要:character
は、出力する文字の ASCII コード値であり、出力する文字を表します。
stream
、ファイル ポインター、読み取りたいファイルのファイル情報領域を指します。
基本的にはfgetc
と同じプロパティですが、入力と出力が 1 つずつある点が異なり、コードを直接入力します
int main(){
FILE* pf = fopen("test.txt", "w");
if (pf == NULL) {
perror("Error opening file");
return 0;
}
//输出26个字母到文件里
for (int i = 0; i < 26; i++) {
fputc('a' + i, pf);
}
fclose(pf);
pf = NULL;
return 0;
}
fgets
は、一度に複数の文字 (つまり、文字列) を読み取ることができるfgetc
のアップグレード バージョンとして理解できます。次のように定義されます。
char * fgets ( char * str, int num, FILE * stream );
戻り値:char*
、関数が正常に実行された場合はパラメータの str
が返され、失敗した場合はが返されます。 a>NULL
ポインタ
パラメータの概要:str
、読み取った文字列を貼り付ける位置
num
、fgets
はファイル内の num-1
文字を読み取り、そのうち num
文字は 文字を配置するために使用されます。あ>あ>\0
stream
、ファイル ポインター、読み取りたいファイルのファイル情報領域を指します。
このコードを実行するには、fputc
で記述された上記の 26 文字のファイルを使用します
int main() {
FILE* pf = fopen("test.txt", "r");
if (pf == NULL) {
perror("Error opening file");
return 0;
}
char str[30];
printf("%s", fgets(str, 27, pf));
fclose(pf);
pf = NULL;
return 0;
}
fputs
は fgets
に似ています。fputc
のアップグレード バージョンとして使用して、次のように定義された文字列を直接出力できます。
int fputs ( const char * str, FILE * stream );
戻り値: 出力が成功した場合は負でない整数を返し、失敗した場合は負でない整数を返します。EOF
パラメータの概要:str
、出力する文字列を指します
stream
、ファイル ポインター、読み取りたいファイルのファイル情報領域を指します。
int main() {
FILE* pf = fopen("test.txt", "w");
if (pf == NULL) {
perror("Error opening file");
return 0;
}
char str[] = "abcdef";
fputs(str, pf);
fclose(pf);
pf = NULL;
return 0;
}
fscanf
ファイル内のデータをフォーマットに従って読み取る関数で、具体的な使い方についてはscanfを参照してください。
int fscanf ( FILE * stream, const char * format, ... );
は実際には scanf
に似ていますが、 scanf
は標準入力ストリームのみの関数であり、これはすべてのストリームの関数である点が異なります。コース ファイル ストリームを含める
の使い方は、ファイル ポインタを提供すること以外は scanf
と変わりません。ファイル データをキーボードで入力したデータとみなすことで、この機能をシームレスに使用できます。
次のデータがファイル内に保存されているとします。
15 male xiaoming
次に、構造体変数を使用してそれを受け取ると、次のコードのように記述できます。
struct person
{
int age;
char gender[10];
char name[20];
};
int main(){
struct person p1;
FILE* pf = fopen("test.txt", "r");
if (NULL == pf) {
perror("fopen");
}
fscanf(pf, "%d %s %s", &(p1.age), p1.gender, p1.name);
printf("%d %s %s", p1.age, p1.gender, p1.name);
fclose(pf);
pf = NULL;
return 0;
}
fprintf
言うまでもなく、上記の fscanf
および scanf
と同じプロパティがあります
printf
これは標準入力ストリームに対する関数であり、ファイルストリームを含むすべてのストリームに対する関数であり、次のように定義されます。
int fprintf ( FILE * stream, const char * format, ... );
同様に、ファイル ストレージを画面として想像すると、この機能をよりよく理解できます。
struct person
{
int age;
char gender[10];
char name[20];
};
int main(){
struct person p1 = {
20, "female", "xiaomei"};
FILE* pf = fopen("test.txt", "w");
if (NULL == pf) {
perror("fopen");
}
fprintf(pf, "%d %s %s", p1.age, p1.gender, p1.name);
fclose(pf);
pf = NULL;
return 0;
}
上記ではすべての出力ストリームに適用される関数をいくつか説明しましたが、これらはすべての出力ストリームに適用される関数であるため、実際にはファイル ストリームの代わりに標準ストリームを直接使用することができます。
次のように
int main() {
char str[] = "hehe";
//直接输出到标准输出流
fputs(str, stdout);
return 0;
}
int main() {
char str[20];
//直接从标准输入流读取
fgets(str,6,stdin);
printf("%s", str);
return 0;
}
ファイル ストリームに適した関数をさらに 2 つ見てみましょう。
fwrite
この関数は、上記関数のように情報をテキスト形式で出力するのではなく、バイナリデータ形式でファイルに出力するために使用されます。
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
戻り値: 出力に成功した要素の数を返します。
パラメータの概要:ptr
、出力するデータのアドレスを指します
size
、出力データ内の単一要素のサイズ
count
、出力される要素の数
stream
、ファイル ポインター、読み取りたいファイルのファイル情報領域を指します。
typedef struct person
{
int age;
char gender[10];
char name[20];
}person;
int main() {
person p1 = {
10,"male","zhangsan"};
FILE* pf = fopen("test.txt","wb");
if (pf == NULL) {
perror("fopen");
return 1;
}
fwrite(&p1, sizeof(p1), 1, pf);
fclose(pf);
pf = NULL;
}
次に、ファイルにバイナリ データを保存します。その後、それをテキスト形式で直接開くと、文字化けが発生します。このとき、内部のデータを読み取れるようにするためです。 、バイナリを使用してフォーム内のデータを読み取ることができ、次のfread
関数が対応する関数を提供します
fread
この関数は、ファイルからバイナリ データ形式で情報を読み取るために使用され、次のように定義されます。
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
戻り値: 正常に読み取られた要素の数を返します。readEOF
return0
パラメータの概要:ptr
は、読み取ったデータが保存される場所を指します。サイズは少なくとも である必要があります。size*count
size
、読み取りデータ内の単一要素のサイズ
count
、読み取られる要素の数
stream
、ファイル ポインター、読み取りたいファイルのファイル情報領域を指します。
typedef struct person
{
int age;
char gender[10];
char name[20];
}person;
int main() {
person p1;
FILE* pf = fopen("test.txt","rb");
if (pf == NULL) {
perror("fopen");
return 1;
}
fread(&p1, sizeof(p1), 1, pf);
printf("%d %s %s", p1.age, p1.gender, p1.name);
fclose(pf);
pf = NULL;
}
ファイルのランダムな読み取りと書き込み
ランダムな読み書きと言われますが、実際には完全にランダムではありません。実は、上記の関数のようにシーケンシャルに読み書きするだけでなく、ファイル位置ポインタの指す位置を制御することで目的のデータを読み込むことができます。
fseek
fseek
これは、シーケンシャルに読み書きするだけでなく、開いているファイル内のファイル位置ポインタを移動して、必要なデータにアクセスするために使用されます。
int fseek ( FILE * stream, long int offset, int origin );
戻り値: 移動が成功した場合は0
を返し、失敗した場合は0
非値を返します。
パラメータの概要:stream
、ファイル ポインタ。ファイルを読み取るファイル情報領域を指します。
offset
、ファイル位置ポインタの移動距離(単位:バイト)、左にオフセットされている場合、 n
ビットが渡されます -n
、右に移動してn
そのまま渡してくださいn
origin
、オフセットoffset
は参照の開始点です。一般的によく使用されるパラメータは次の 3 つです。
パラメータ名 | パラメータの意味 |
---|---|
SEEK_SET | ファイルの先頭 |
SEEK_CUR | ファイル位置ポインタの現在位置 |
SEEK_END | ファイルの終わり |
次の情報がファイルに保存されているとします。
abcdef
#include<stdio.h>
int main() {
FILE* pf = fopen("test.txt","r");
if (NULL == pf) {
perror("fopen");
return 1;
}
//顺序读4个字符,此时指向e
for (int i = 0;i < 4;i++) {
fgetc(pf);
}
//让文件指针向左偏移三个
fseek(pf, -3, SEEK_CUR);
//这个时候下面就会读字符b
printf("%c", fgetc(pf));
fclose(pf);
pf = NULL;
return 0;
}
ftell
ファイルの先頭を基準とした現在のファイル位置ポインタのオフセットを示す関数。次のように定義されます。
long int ftell ( FILE * stream );
シンプルすぎて詳細があまりない
rewind
ファイル位置ポインタをファイルの先頭にリセットする関数。次のように定義されます。
void rewind ( FILE * stream );
シンプルすぎて詳細があまりない
ファイル読み込み終了の判定
ここでの多くの情報は、feof
ファイル位置ポインタが読み取られたかどうかを判断するためにEOF
使用できることを示しています。ファイルが読み取られたかどうかを確認し、最後まで処理します。しかし、実際にはそうではありません。まずfeof
この関数を理解しましょう。
feof
ファイル読み取り操作の完了後にEOF
インジケータを検出するために使用されます
int feof ( FILE * stream );
戻り値: EOF指示器
が検出された場合は負でない数値が返され、それ以外の場合は負でない数値が返されます0
パラメータの概要:stream
、ファイル ポインタ。ファイルを読み取るファイル情報領域を指します。
EOFインジケーターとは何ですか?
通常、ファイルがファイルの最後まで読み取られると、EOF
が読み取られ、その後、プログラムは **end-of-file indicator
* を次の位置に設定します。今回は*、つまりです。EOF指示器
読み取り失敗後など、ファイルが異常終了すると、プログラムは **error indicator
**、つまり 错误指示器
を設定します。 、この 错误指示器
は、 ferror
関数、 ferror
を使用して検出できますが、検出されたもの、残りのパラメータと戻り値が異なります。似ています。の
要約すると、feof
は の検出ではなく、EOF
インジケーターの検出に使用されます。また、 インジケータは、ファイルの読み取り操作が完了した後にのみ設定されます。つまり、 は、ファイルの読み取りが完了した後にのみ、ファイルの読み取り操作が完了したかどうかを判断するために使用できます。正常です。読み取りが終了したか、読み取り失敗により終了しました。EOF
EOF
feof
EOF
正しい使用例をいくつか示しますfeof
fgetc
そしてfeof
のでfgetc
は読み込み失敗時やファイルの最後まで読み込んだ場合はEOF
を返しますので、この際に<があるかどうかを確認します。 /span>EOF指示器
ファイルの読み取りが正常に終了したかどうかを判断します
int main() {
FILE* pf = fopen("test.txt", "r");
if (pf == NULL) {
perror("fopen");
return 1;
}
while (fgetc(pf) != EOF) {
}
//判断哪一个指示器被设立了
if (feof){
printf("读取成功\n");
}
else if (ferror) {
printf("读取失败\n");
}
fclose(pf);
pf = NULL;
return 0;
}
fgets
そしてfeof
ファイルの最後まで読み取るとき、および読み取りが失敗した場合、fgets
はNULL
ポインタを返すため、インジケーターも使用する必要があります。正常かどうかを判断します。終了
int main() {
FILE* pf = fopen("test.txt", "r");
if (pf == NULL) {
perror("fopen");
return 1;
}
char tmp[20];
while (fgets(tmp, 3, pf) != NULL) {
}
//判断哪一个指示器被设立了
if (feof) {
printf("读取成功\n");
}
else if (ferror) {
printf("读取失败\n");
}
fclose(pf);
pf = NULL;
return 0;
}
fread
そしてfeof
データの読み取りを要求したときfread
、ファイル内に不十分なデータがある場合、戻り値は読み取りを要求したデータ量よりも少なくなりますが、読み取りは失敗します。また、戻り値も返されます。これは、読み取る必要があるデータの量よりも少ないため、現時点では、 feof
を使用して、読み取りの終了によって終了するのか、それとも終了するのかを判断する必要があります。読み取りの終了に失敗しました
int main() {
FILE* pf = fopen("test.txt", "wb");
if (pf == NULL) {
perror("fopen");
return 1;
}
//假设文件中存的就是 tmp存的数据
char tmp[] = "abdbfbfhja";
int count = sizeof(tmp) + 5;
int test = fread(tmp, 1, count, pf);
if (count == test) {
printf("读取正常\n");
}
else {
printf("读取异常,开始检测异常\n");
if (feof) {
printf("读取到EOF\n");
}
else if (ferror) {
printf("文件读取失败\n");
}
}
fclose(pf);
pf = NULL;
return 0;
}
アドレス帳データストレージの実装
上記のように基礎を築いたので、アドレス帳の最終バージョンの実装を正式に開始できます。
コアは 2 つの関数fread
と fwrite
を使用します。これは、コアのパラメータが構造の保存に最適であるためです。
実際、追加する必要がある関数は次の 2 つだけです: 1. 破棄する前にデータを保存する 2. 初期化中にデータをロードする
データの保存
データが保存されるため、fwrite
関数 を使用する必要があります。
同時に、データを一度に 1 つずつファイルに保存することを選択します。
void SaveContact(Contact* p) {
FILE* pf = fopen("data.dat", "wb");
if (NULL == pf) {
perror("Save error");
return;
}
for (int i = 0; i < p->dataNum; i++) {
fwrite(p->data + i, sizeof(Peopleinfo), 1, pf);
}
fclose(pf);
pf = NULL;
}
すべてを一度にファイルに保存したい場合は、保存されたコードを次のコードに変更できます。
fwrite(p->data, sizeof(Peopleinfo)*(p->dataNum), 1, pf);
読み取りデータ
データを読み取る際には、考慮すべき要素が数多くあります。
まず、初期化したばかりでデータの量がわからないため、すべてのデータを一度に読み取ることは確かに不可能です。次に、初期容量が限られているため、データを追加し、同時に容量を拡張するかどうかを決定する必要があります。最後に、データの変化に応じて増加する必要もありますdataNum
(つまり、有効なデータの数)
もちろん、データを保存するときにコピーを保存することも選択できますdataNum
。ここでは主に、保存がない状況について説明しますdataNum
最初にファイルを開く方法を決定する必要があります。初めて使用する場合はデータが存在しないことを考慮して、ファイルを作成する必要があります。次に、その後の使用中にプログラムにファイルを消去させることはできないため、 "ab+"
読み取りおよび書き込みメソッドを使用する必要があります。ただし、この読み取りおよび書き込み方法を使用する場合、ファイル位置ポインタは最初にファイルの末尾を指し、その後、別の rewind
を使用してファイルの先頭に戻します。
FILE* pf = fopen("data.dat", "ab+");
rewind(pf);
それではfread
データを1つずつ読み込んでいくので、ループを使う必要があります。ループの判定はどのように書けばよいでしょうか?
データを読み取れない場合fread
は、ループを停止する必要があります。上記の知識に基づいて、fread
データがファイルの終わり 読み込みは続行されません。このとき、読み込んだデータ数を表す戻り値は 0
となるので、 < の戻り値をそのまま使用できます。 a i=4> をループとして 判定条件fread
同時に、fread
の構造体が指すスペースに読み取ったデータを直接置くことはできません。なぜですか?
例を示します。ファイルに保存されているデータの数が 3 より大きい場合、3 つの要素を追加すると、この時点ではdataNum
は 3 に等しく、コードが 3 ラウンドに進むと、初期の最大ストレージ容量に達し、理論的にはデータを保存するために拡張が必要になります。ただし、データ ストレージをループ判定に直接配置するため、この時点では、コードは拡張コードを実行する前に 4 番目のデータ セットをスペースに配置する必要があり、これにより 2 つの致命的な問題が発生します。
- 未定義空間の操作は不正アクセスとなります。
- メモリ操作の競合
realloc
関数を展開すると、操作した領域に遭遇します
すると今度はスタック領域にダメージが発生します。
したがって、コード全体は次のように書く必要があります
int LoadContactData(Contact* p)
{
//打开文件
FILE* pf = fopen("data.dat", "ab+");
rewind(pf);
if (NULL == pf) {
perror("Load failure");
return 1;
}
//拿取数据
Peopleinfo tmp = {
0 };
int i = 0;
while (i = fread(&tmp, sizeof(Peopleinfo), 1, pf)) {
//判断
if (!i) {
if (!feof(pf)) {
printf("数据读取出错\n");
return 1;
}
}
//扩容
if (p->dataNum == p->capacity) {
if (IncreaseContact(p)) {
printf("扩容失败\n");
return 1;
}
}
//存放
p->data[p->dataNum] = tmp;
p->dataNum++;
}
//关闭文件和置空指针
fclose(pf);
pf = NULL;
return 0;
}
要約する
ということで、3記事を経て、ようやくアドレス帳が完成しました。
実際には、このファイル操作のアドレス帳部分を記述する方法は数多くあります。たとえば、データを保存するときにコピーを保存しdataNum
、読み取るときに変更を加えます。これにより、データを取得したときにそれを直接保存できます。ここでは私がアイデアを共有しているだけです。残りは読者が自分で調べてください
コードの概要
contact.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"contact.h"
static int IncreaseContact(Contact* p);
int LoadContactData(Contact* p)
{
FILE* pf = fopen("data.dat", "ab+");
rewind(pf);
if (NULL == pf) {
perror("Load failure");
return 1;
}
Peopleinfo tmp = {
0 };
int i = 0;
while (i = fread(&tmp, sizeof(Peopleinfo), 1, pf)) {
if (!i) {
if (!feof(pf)) {
printf("数据读取出错\n");
return 1;
}
}
if (p->dataNum == p->capacity) {
if (IncreaseContact(p)) {
printf("扩容失败\n");
return 1;
}
}
p->data[p->dataNum] = tmp;
p->dataNum++;
}
fclose(pf);
pf = NULL;
return 0;
}
int InitContact(Contact* p) {
p->data = (Peopleinfo*)malloc(DEFAULT_SZ * sizeof(Peopleinfo));
if (p->data == NULL) {
printf("通讯录初始化失败:%s", strerror(errno));
return 1;
}
p->dataNum = 0;
p->capacity = DEFAULT_SZ;
if (LoadContactData(p))
{
return 1;
}
return 0;
}
static int IncreaseContact(Contact* p) {
Peopleinfo* ptr = (Peopleinfo*)realloc(p->data, (p->capacity + INCREACE_SZ) * sizeof(Peopleinfo));
if (ptr == NULL) {
printf("通讯录扩容失败:%s", strerror(errno));
return 1;
}
p->capacity += INCREACE_SZ;
p->data = ptr;
return 0;
}
void AddContact(Contact* p) {
if (p->dataNum == p->capacity) {
if (IncreaseContact(p)) {
printf("扩容失败\n");
return;
}
}
printf("接下来请按照指示输入各项数据\n");
printf("请输入姓名\n");
scanf("%s", (p->data)[p->dataNum].name);
printf("请输入性别\n");
scanf("%s", (p->data)[p->dataNum].gender);
printf("请输入年龄\n");
scanf("%d", &((p->data)[p->dataNum].age));
printf("请输入电话\n");
scanf("%s", (p->data)[p->dataNum].tele);
printf("请输入地址\n");
scanf("%s", (p->data)[p->dataNum].addr);
p->dataNum++;
printf("添加成功\n\n\n");
}
static void printContact(const Contact* p, int num) {
printf("%-10s %-5s %-4d %-12s %-30s\n",
(p->data)[num].name
, (p->data)[num].gender
, (p->data)[num].age
, (p->data)[num].tele
, (p->data)[num].addr);
}
void ShowContact(const Contact* p) {
if (p->dataNum) {
printf("%-10s %-5s %-4s %-12s %-30s\n", "姓名", "性别", "年龄", "电话", "地址");
for (int i = 0; i < p->dataNum; i++) {
printContact(p, i);
}
}
else {
printf("数据为空,请先存放一些数据在进行该操作\n");
}
printf("\n\n\n");
}
static FindByName(const Contact* p, char name[MAX_NAME]) {
for (int i = 0; i < p->dataNum; i++) {
if (!strcmp(name, (p->data)[i].name)) {
return i;
}
}
return -1;
}
void SearchContact(const Contact* p) {
if (p->dataNum) {
char name[MAX_NAME] = {
0 };
printf("请输入要查找人的名字\n");
scanf("%s", name);
int i = FindByName(p, name);
if (i != -1) {
printf("查找成功\n");
printf("%-10s %-5s %-4s %-12s %-30s\n", "姓名", "性别", "年龄", "电话", "地址");
printContact(p, i);
}
else {
printf("未查询到目标\n");
}
}
else {
printf("数据为空,请先存放一些数据在进行该操作\n");
}
printf("\n\n\n");
}
void DelContact(Contact* p) {
if (p->dataNum != 0) {
char name[MAX_NAME] = {
0 };
printf("请输入要删除人的名字\n");
scanf("%s", name);
int pos = FindByName(p, name);
if (pos != -1) {
while (pos < p->capacity - 1) {
p->data[pos] = p->data[pos + 1];
pos++;
}
memset(&(p->data[pos]), 0, sizeof(p->data[pos]));
p->dataNum--;
printf("删除成功\n");
}
else {
printf("要找的人不存在\n");
}
}
else {
printf("数据为空,请先存放一些数据在进行该操作\n");
}
printf("\n\n\n");
}
void ModifyContact(Contact* p) {
if (p->dataNum) {
char name[MAX_NAME] = {
0 };
printf("请输入要修改数据的人名\n");
scanf("%s", name);
int i = FindByName(p, name);
if (i != -1) {
printf("接下来请按照指示输入各项数据\n");
printf("请输入姓名\n");
scanf("%s", (p->data)[i].name);
printf("请输入性别\n");
scanf("%s", (p->data)[i].gender);
printf("请输入年龄\n");
scanf("%d", &((p->data)[i].age));
printf("请输入电话\n");
scanf("%s", (p->data)[i].tele);
printf("请输入地址\n");
scanf("%s", (p->data)[i].addr);
printf("修改成功\n\n\n");
}
else {
printf("未查询到目标\n");
}
}
else {
printf("数据为空,请先存放一些数据在进行该操作\n");
}
printf("\n\n\n");
}
static int SortByName(void* p1, void* p2) {
return strcmp((char*)p1, (char*)p2);
}
void SortContact(Contact* p) {
if (p->dataNum) {
qsort(p->data, p->dataNum, sizeof(p->data[0]), SortByName);
printf("排序成功\n\n");
ShowContact(p);
}
else {
printf("数据为空,请先存放一些数据在进行该操作\n");
printf("\n\n\n");
}
}
void DestroyContact(Contact* p) {
free(p->data);
p->dataNum = 0;
p->capacity = 0;
p->data = NULL;
}
void SaveContact(Contact* p) {
FILE* pf = fopen("data.dat", "wb");
if (NULL == pf) {
perror("Save error");
return;
}
for (int i = 0; i < p->dataNum; i++) {
fwrite(p->data + i, sizeof(Peopleinfo), 1, pf);
}
fclose(pf);
pf = NULL;
}
main.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"contact.h"
void menu() {
printf("********************************\n");
printf("***** 1.ADD 2.DEL *****\n");
printf("***** 3.SEARCH 4.MODIFY *****\n");
printf("***** 5.SHOW 6.SORT *****\n");
printf("***** 0.EXIT *****\n");
printf("********************************\n");
}
enum Option
{
EXIT,
ADD,
DEL,
SEARCH,
MODIFY,
SHOW,
SORT
};
int main() {
Contact con;
if (InitContact(&con)) {
return 0;
}
int input = 0;
do {
printf("请选择操作\n");
menu();
scanf("%d", &input);
switch (input)
{
case ADD:
AddContact(&con);
break;
case DEL:
DelContact(&con);
break;
case SEARCH:
SearchContact(&con);
break;
case MODIFY:
ModifyContact(&con);
break;
case SHOW:
ShowContact(&con);
break;
case SORT:
SortContact(&con);
break;
case EXIT:
SaveContact(&con);
DestroyContact(&con);
printf("退出通讯录\n");
break;
default:
printf("选择错误,请重新选择\n");
break;
}
} while (input);
return 0;
}
contact.h
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<errno.h>
#define MAX_NAME 20
#define MAX_GENDER 5
#define MAX_TELE 12
#define MAX_ADDR 30
#define DEFAULT_SZ 3
#define INCREACE_SZ 2
typedef struct Peopleinfo
{
char name[MAX_NAME];
char gender[MAX_GENDER];
int age;
char tele[MAX_TELE];
char addr[MAX_ADDR];
}Peopleinfo;
typedef struct Contact
{
//存储有效数据个数
int dataNum;
//指向存贮数据的空间
Peopleinfo* data;
//记录最大容量
int capacity;
}Contact;
//初始化通讯录
int InitContact(Contact* p);
//添加功能
void AddContact(Contact* p);
//展示通讯录
void ShowContact(const Contact* p);
//查找功能
void SearchContact(const Contact* p);
//删除功能
void DelContact(Contact* p);
//编辑通讯录
void ModifyContact(Contact* p);
//排序通讯录
void SortContact(Contact* p);
//销毁通讯录
void DestroyContact(Contact* p);
//保存通讯录
void SaveContact(Contact* p);