Notes

16進数

  • Week 2では、メモリと、各バイトがどのようにアドレスまたは識別子を持っているかについて話し、データが実際に格納されている場所を参照できるようにしました。
  • 慣習的に、メモリのアドレスは16進法を使っていることが分かっています。16進数は、0-9、A-Fを使った16の数字で構成されていて、A-Fは10-15を表しています。
  • 2桁の16進数を考えてみましょう。
16^1 16^0
   0    A
  • ここでは、 (16^0=1であるため) 1の位のAの10進数は10です。0F (10進数で15に相当) までカウントできます。
  • 0F の後は、10進数で09から10を表すように、1を繰り上げる必要があります。
16^1 16^0
   1    0
  • ここで、1の値は 16^1*1=16 なので、16進数の 10 は10進数で言うところの16になります。
  • 2桁の場合、FF の最大値、すなわち16^1*15+16^0*15=240+15=255を得ることができ、これは8ビットの2進数と同じ最大値です。したがって、16進数の2桁は、1バイトの値を2進数で表すのに便利です (16の値を持つ16進数の各桁は、2進数の4ビットにマッピングされます) 。
  • 文書では値が16進数であることを示すために0x で始まり、0x10 の場合であれば10進数の10ではなく16であり、16進数としての10であることを示します。
  • RGBカラーシステムでは、従来、各カラーの量を16進数で表していました。たとえば、16進数の 000000 は、赤、緑、青のそれぞれに対して0を表し、組み合わさった黒がその色になります。FF0000 だと赤色の最大値255になります。FFFFFF は各色の最大値を示し、組み合わされうと最も明るい白になります。色ごとに異なる値を使用すると、何百万もの異なる色を表現できます。
  • コンピュータのメモリについても、アドレスまたは場所ごとに16進数を使用します。

アドレス

  • nを作成して出力します。
#include <stdio.h>

int main(void)
{
    int n = 50;
    printf("%i\n", n);
}
  • コンピュータのメモリには、50を2進数(ビット列)で格納した4バイトがどこかにあり、nと名前付けされています。
grid representing bytes, with four boxes together containing 50 with small n underneath
  • メモリは何十億バイトもあるので、変数 n のバイトは 0x12345678 のようなどこかから始まることがわかります。
  • Cでは、実際には & 演算子でアドレスを確認できます。これは、 「この変数のアドレスを取得する」 ことを意味します。
#include <stdio.h>

int main(void)
{
    int n = 50;
    printf("%p\n", &n);
}
  • %p はアドレスの書式コードです。
    • CS50 IDEでは、0x7ffd80792f7c のようなアドレスが表示されます。アドレスの値自体は、変数が格納されているメモリ内の単なる場所であるため、有用ではありません。このアドレスを後で使用できるようにすることが重要です。
  • * 演算子 (間接参照演算子) を使用すると、ポインタが指している場所に 「行く」事ができます。
  • たとえば、*&n を出力するとした時は、n のアドレスに 行き、 アドレス n の値である 50 が出力されます。
#include <stdio.h>

int main(void)
{
    int n = 50;
    printf("%i\n", *&n);
}

ポインタ

  • アドレスを格納する変数はポインタと呼ばれ、メモリ内の場所を 「参照する」 値と考えることができます。Cでは、ポインタは特定の型の値を参照できます。
  • * 演算子を (残念ながら紛らわしい方法で) 使用して、ポインタにしたい変数を宣言することができます。
#include <stdio.h>

int main(void)
{
   int n = 50;
   int *p = &n;
   printf("%p\n", p);
}
  • ここでは、int *p を使用して、整数であるint型へのポインタを宣言します。次に、その (アドレス 0x12345678 など) を printf("%p\n", *p); で出力しています。
  • コンピュータのメモリでは、変数は次のようになります。
  • p は変数自体なので、メモリのどこかにあり、そこに格納されている値は n のアドレスです。
    • 最近のコンピュータシステムは 「64ビット」 です。つまり、メモリのアドレス指定に64ビットを使用するので、ポインタは実際には8バイトになり、4バイトの整数の2倍の大きさになります。
  • アドレスの実際の値を抽象化することができます。なぜなら、プログラムで変数を宣言するときにはアドレスは異なり、あまり有用ではなく、単に p はある値を 「指し示す」、「参照する」 と考えるからです。
  • 現実の世界では、アドレス付きの多くのメールボックスの中に 「p」 というラベルの付いたメールボックスがある場合があります。メールボックスの中には 0x123 のような値を入れることができます。これは他のメールボックス n のアドレスで、アドレスは 0x123 です。

文字列

string s = "HI!"; と宣言された変数は一度に1文字ずつメモリに格納されます。s[0]s[1]s[2]s[3] を使って各文字にアクセスできます。

しかし、それぞれの文字はメモリに格納されているため、それぞれが固有のアドレスを持ち、s は実際には最初の文字のアドレスを持つ単なるポインタであることがわかります。

変数sには、文字列の最初の文字のアドレスが格納されます。値 \0 は、文字列の末尾を示す唯一のインジケータです。

  • 残りの文字は連続した配列ですから、s のアドレスから始めて、\0 に達するまでメモリから一度に1文字ずつ読み続けることができます。
  • 文字列を出力しましょう。
#include <cs50.h>
#include <stdio.h>

int main(void)
{
    string s = "HI!";
    printf("%s\n", s);
}

s に格納された値は printf("%p\n", s); で、文字列の最初の文字のアドレスをメモリに出力しているので、0x4006a4 のように表示されます。

printf("%p\n", &s[1]); で別の行を追加すると、メモリ内の次のアドレス 0x4006a5 が表示されます。

•文字列 s はメモリ上のある文字へのポインタであることがわかります。

•実際、CS50ライブラリでは、Cでは存在しない string という型を char * として typedef char *string; で定義しています。カスタム型stringは、typedefで単なるchar *として定義されます。したがって、string s = "HI!"char *s = "HI!"; と同じです。また、char * を使用することで、CS50ライブラリがない場合とまったく同じ方法でCの文字列を使用できます。

ポインタ演算

ポインタ演算とは、ポインタを使ってアドレスに対し数学的演算を行うことです。

•文字列の各文字を出力することができます ( char * を直接使用)

#include <stdio.h>

int main(void)
{
    char *s = "HI!";
    printf("%c\n", s[0]);
    printf("%c\n", s[1]);
    printf("%c\n", s[2]);
}
  • アドレスを直接指定することもできます。
#include <stdio.h>

int main(void)
{
    char *s = "HI!";
    printf("%c\n", *s);
    printf("%c\n", *(s+1));
    printf("%c\n", *(s+2));
}
  • *ss に格納されているアドレスに、*(s+1) は1バイト上位のアドレス、つまり次の文字を持つメモリ内の場所を参照します。s[1]*(s+1) の糖衣構文で、機能的には同じですが、人間にとっては読み書きしやすいものです。
  • *(s+10000) のように、メモリ内のアクセスするべきではないアドレスにアクセスすることもできます。プログラムを実行すると、セグメンテーション違反が発生したり、プログラムが不要なセグメントのメモリにアクセスした結果クラッシュしたりします。

比較とコピー

  • ユーザ入力からの2つの整数を比較してみましょう。
#include <cs50.h>
#include <stdio.h>

int main(void)
{
    int i = get_int("i: ");
    int j = get_int("j: ");

    if (i == j)
    {
        printf("Same\n");
    }
    else
    {
        printf("Different\n");
    }
}
  • プログラムをコンパイルして実行すると、予想どおりに動作し、2つの整数が同じ値であれば 「Same (同じ)」 、異なる値であれば 「Different (異なる)」 と表示されます。
  • 2つの文字列を比較しようとすると、同じ入力でもプログラムが 「Different」 と出力していることがわかります。
#include <cs50.h>
#include <stdio.h>

int main(void)
{
    char *s = get_string("s: ");
    char *t = get_string("t: ");

    if (s == t)
    {
        printf("Same\n");
    }
    else
    {
        printf("Different\n");
    }
}
  • 入力が同じでも、 「Different」 と表示されます。
    • 各 「文字列」 は、メモリ内の異なる場所へのポインタ char * です。各文字列の最初の文字が格納されます。したがって、文字列内の文字が同じであっても、常に 「異なる」 と表示されます。
  • たとえば、最初の文字列はアドレス0x123 、2番目の文字列は 0x456 、s はその場所を示す値 0x123t は別の場所を示す値 0x456 になります。
  • そして get_string は、これまでずっと char * 、すなわちユーザからの文字列の最初の文字へのポインタだけを返してきました。get_string を2回呼び出したため、2つの異なるポインタが返されました。
  • 文字列をコピーしてみましょう。
#include <cs50.h>
#include <ctype.h>
#include <stdio.h>

int main(void)
{
    char *s = get_string("s: ");

    char *t = s;

    t[0] = toupper(t[0]);

    printf("s: %s\n", s);
    printf("t: %s\n", t);
}
  • 文字列 s を取得し、s の値をtにコピーします。次に、t の最初の文字を大文字にします。
    • しかし、プログラムを実行すると、st の両方が大文字になっています。
    • st に同じ値または同じアドレスを設定したので、両方とも同じ文字を指しているため、メモリ内で同じ文字を大文字にしました。
  • 実際に文字列のコピーを作成するには、もう少し作業を行い、s の各文字をメモリ内の別の場所にコピーする必要があります。
#include <cs50.h>
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void)
{
    char *s = get_string("s: ");

    char *t = malloc(strlen(s) + 1);

    for (int i = 0, n = strlen(s); i < n + 1; i++)
    {
        t[i] = s[i];
    }

    t[0] = toupper(t[0]);

    printf("s: %s\n", s);
    printf("t: %s\n", t);
}
  • char * 型の新しい変数 tchar *t で作成します。次に、文字列のコピーを保存するのに十分な大きさの新しいメモリ領域を参照します。malloc では、 (他の値を格納するために使用されていない) メモリ内のバイト数を割り当て、使用する分だけのバイト数を渡します。s の長さはすでにわかっているので、終端のヌル文字の長さに1を加えます。したがって、コードの最終行は char *t = malloc(strlen(s) + 1); となります。
    • 次に、for ループを使用して各文字を1つずつコピーします。i < n + 1 を使用するのは、実際に文字列の長さであるnまで行って、文字列内の終了文字を確実にコピーするためです。ループ内で t[i] = s[i] を設定し、文字をコピーします。同じ効果を得るために、*(t+i) = *(s+i) を使用することもできますが、読みやすさが劣ることは間違いありません。
    • これで、t の最初の文字だけを大文字にできます。
  • プログラムにエラーチェックを追加できます。
#include <cs50.h>
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void)
{
    char *s = get_string("s: ");

    char *t = malloc(strlen(s) + 1);
    if (t == NULL)
    {
        return 1;
    }

    for (int i = 0, n = strlen(s); i < n + 1; i++)
    {
        t[i] = s[i];
    }

    if (strlen(t) > 0)
    {
        t[0] = toupper(t[0]);
    }

    printf("s: %s\n", s);
    printf("t: %s\n", t);

    free(t);
}
  • コンピュータのメモリが不足している場合、malloc は NULL (NULLポインタ、参照するアドレスがないことを示す特別な値) を返します。その場合を調べ、tNULL ならば終了します。
    • 最初の文字を大文字にする前に、t が長さを持っていることをチェックすることもできます。
    • 最後に、前に割り当てたメモリを解放し、他のプログラムが再び使用できるようにします。free関数を呼び出し、ポインタ t を渡します。これでメモリの(整地)チャンクは終了です ( get_string も、malloc を呼び出して文字列にメモリを割り当て、main 関数が戻る直前に free を呼び出します) 。
  • 実際にはループの代わりに、Cの文字列ライブラリにある strcpy 関数を s strcpy(t, s); として利用し、文字列 st にコピーします。

valgrind (ヴァルグリンド)

  • valgrind は、プログラムを実行し、メモリリークが発生していないか、解放せずに割り当てたメモリがあるかどうかを確認するために使用できるコマンドラインツールです。メモリリークにより、最終的にコンピュータのメモリが不足する可能性があります。
  • memory.c では、文字列を作成しますが、メモリに必要な量よりも少ない割り当てを行います。
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    char *s = malloc(3);
    s[0] = 'H';
    s[1] = 'I';
    s[2] = '!';
    s[3] = '\0';
    printf("%s\n", s);
}
  • 割り当てたメモリも解放しません。
    • コンパイル後に valgrind ./memory を実行すると、多くの出力が表示されますが、help50 valgrind ./memory を実行して、これらのメッセージの一部を説明します。このプログラムでは、 「Invalid write of size 1」 、 「Invalid read of size 1」 、そして最後に 「3 bytes in 1 blocks are definitely lost」 のような断片があり、近くに行番号があります。実際、メモリに s[3] を書き込みますが、これは最初に s に割り当てた内容の一部ではありません。また、s を印刷するときには、s[3] までのすべての部分を読むことになります。最後に、s はプログラムの終了時に解放されません。
  • 適切なバイト数を割り当て、最後にメモリを解放することができます。
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    char *s = malloc(4);
    s[0] = 'H';
    s[1] = 'I';
    s[2] = '!';
    s[3] = '\0';
    printf("%s\n", s);
    free(s);
}
  • valgrind は警告メッセージを表示しません。

ゴミの値

  • 以下を見てみましょう。
int main(void)
{
    int *x;
    int *y;

    x = malloc(sizeof(int));

    *x = 42;
    *y = 13;

    y = x;

    *y = 13;
}
  • 整数 xy への2つのポインタを宣言しますが、それらに値を割り当てません。malloc を使って sizeof(int) を持つ整数に十分なメモリを割り当て、それをxに格納します。*x = 42 は、x が指すアドレスに行き、メモリ内のその位置の値を42に設定します。
    • *y = 13 で、y が指すアドレスに値13を配置しようとしています。しかし、y に値を代入したことがないので、これまでコンピュータで実行されていたプログラムからのゴミの値やメモリ内の未知の値があります。したがって、アドレスとして y のゴミの値を参照しにいこうとすると、不明なアドレスを参照しようとすることになり、セグメンテーション違反 (segfault)が発生する可能性があります。
  • Pointer Fun with Binkyという動画で、上のコードのコンセプトを紹介しています。
  • 配列を宣言し、その値を設定しないことで、不要な値を出力することができます。
#include <stdio.h>

int main(void)
{
    int scores[3];
    for (int i = 0; i < 3; i++)
    {
        printf("%i\n", scores[i]);
    }
}
  • このプログラムをコンパイルして実行すると、さまざまな値が表示されます。

スワップ

  • 2つの整数の値を入れ替えてみましょう。
#include <stdio.h>

void swap(int a, int b);

int main(void)
{
    int x = 1;
    int y = 2;

    printf("x is %i, y is %i\n", x, y);
    swap(x, y);
    printf("x is %i, y is %i\n", x, y);
}

void swap(int a, int b)
{
    int tmp = a;
    a = b;
    b = tmp;
}
  • 現実の世界では、1つのグラスに赤い液体が入っていて、別のグラスに青い液体が入っていて、それらを交換したい場合、液体の1つ (おそらく赤いグラス) を一時的に保持するために3番目のグラスが必要になります。青い液体を最初のグラスに注ぎ、最後に赤い液体を一時的なグラスから2番目のグラスに注ぎます。
    • swap 関数には、一時記憶領域として使用する第3の変数があります。tmpa を入れ、ab の値に設定し、最後に ba の元の値に変更することができます。
  • しかし、この関数をプログラムで使用しようとしても、何も変更はありません。swap 関数は、渡されたときに独自の変数 ab ( xy のコピー) を取得するため、これらの値を変更してもメイン関数の xy は変更されません。

メモリレイアウト

  • コンピュータのメモリ内では、プログラム用に格納する必要があるさまざまなタイプのデータが、さまざまなセクションに編成されています。
  • 機械語セクションは、コンパイルされたプログラムのバイナリコードです。プログラムを実行すると、そのコードがメモリの 「トップ」 にロードされます。
    • すぐ下、メモリの次の部分には、プログラムで宣言したグローバル変数が配置されます。
    • ヒープセクションは、malloc がプログラムで使用するための空きメモリを取得できる空の領域です。malloc を呼び出すと、トップダウンでメモリの割り当てを開始します。
    • スタックセクションは、呼び出されたプログラム内の関数によって使用され、上に向かって成長します。たとえば、main 関数はスタックの一番下にあり、ローカル変数 xy を持っています。スワップ関数は、呼び出されると main の上に独自のメモリ領域を持ち、ローカル変数 ab 、および tmp を持ちます。
  • 関数 swap が終了すると、使用していたメモリは次の関数呼び出しのために解放されます。xy は引数であるため、swapab としてコピーされるため、変更が main に反映されることはありません。
  • xy のアドレスを渡すことで、swap 関数は実際に動作します。
#include <stdio.h>

void swap(int *a, int *b);

int main(void)
{
    int x = 1;
    int y = 2;

    printf("x is %i, y is %i\n", x, y);
    swap(&x, &y);
    printf("x is %i, y is %i\n", x, y);
}

void swap(int *a, int *b)
{
    int tmp = *a;
    *a = *b;
    *b = tmp;
}
  • xy のアドレスは main から &x&yswap して渡され、int *a 構文を使ってswap関数がポインタを取ることを宣言します。xの値をポインタaに従って tmp に保存し、y の値をポインタ b に従って取得して、a が指している場所 (x) に保存します。最後に、tmp の値を b (y) が指す場所に格納します。
  • 大量のメモリを確保する malloc を呼び出すと、ヒープオーバーフローしてしまいます。あるいは、関数から復帰せずに呼び出した関数が多すぎると、スタックに割り当てられたメモリが多すぎるスタックオーバーフローが発生します。
  • 関数を呼び出して、マリオのピラミッドを描画します。
#include <cs50.h>
#include <stdio.h>

void draw(int h);

int main(void)
{
    int height = get_int("Height: ");
    draw(height);
}

void draw(int h)
{
    for (int i = 1; i <= h; i++)
    {
        for (int j = 1; j <= i; j++)
        {
            printf("#");
        }
        printf("\n");
    }
}
  • draw を再帰的に利用できます。
void draw(int h)
{
    draw(h - 1);

    for (int i = 0; i < h; i++)
    {
        printf("#");
    }
    printf("\n");
}
  • make を使用してこれをコンパイルしようとすると、draw 関数が停止せずに再帰的に呼び出されるという警告が表示されます。このため、追加のチェックを行わずに clang を使用します。このプログラムを実行すると、すぐにセグメンテーション違反が発生します。draw が何度も呼び出され、スタックのメモリが足りなくなりました。
  • 基本ケースを追加すると、draw 関数はある時点で自身を呼び出さなくなります。
void draw(int h)
{
    if (h == 0)
    {
        return;
    }

    draw(h - 1);

    for (int i = 0; i < h; i++)
    {
        printf("#");
    }
    printf("\n");
}
  • しかし、2000000000 のように高さに大きな値を入力した場合、draw を何度も呼び出しても終了しないため、メモリが不足します。
  • バッファオーバーフローは、、配列のように割り当てたメモリの領域であるバッファの末尾を超え、存在すべきでないメモリへのアクセスを行うと発生します。

scanf

  • Cライブラリ関数 scanf を使って get_int を実装することができます。
#include <stdio.h>

int main(void)
{
    int x;
    printf("x: ");
    scanf("%i", &x);
    printf("x: %i\n", x);
}
  • scanf はフォーマット %i を取り、入力はそのフォーマットのために 「スキャン」 されます。また、メモリー内のアドレスを渡して、そのアドレスに入力を渡します。しかし、scanf にはエラーチェック機能があまりないため、整数を取得できない場合があります。
  • 同じ方法で文字列を取得できます。
#include <stdio.h>

int main(void)
{
    char *s;
    printf("s: ");
    scanf("%s", s);
    printf("s: %s\n", s);
}
  • しかし、実際にはsにメモリを割り当てていないので、malloc を呼び出して文字列にメモリを割り当てる必要があります。char s[4]; を使用して4文字の配列を宣言します。次に、s は scanf と printf の最初の文字へのポインタとして扱われます。
    • これで、ユーザが長さ3以下の文字列を入力しても、プログラムは安全に動作します。しかし、ユーザが長い文字列を入力すると、scanf は配列の終端を越えて未知のメモリに書き込もうとし、プログラムをクラッシュさせてしまうかもしれません。
    • CS50ライブラリの get_string は、scanf がより多くの文字を読み込むときにより多くのメモリを継続的に割り当てるため、この問題は発生しません。

ファイル

  • ポインタを使用する機能により、デジタル電話帳などのファイルを開くこともできます。
#include <cs50.h>
#include <stdio.h>
#include <string.h>

int main(void)
{
    FILE *file = fopen("phonebook.csv", "a");
    if (file == NULL)
    {
        return 1;
    }

    char *name = get_string("Name: ");
    char *number = get_string("Number: ");

    fprintf(file, "%s,%s\n", name, number);

    fclose(file);
}
  • fopen は、ファイルを開くために使用できる新しい関数です。この関数は、読み書きできる新しい型FILEへのポインタを返します。最初の引数はファイル名で、2番目の引数はファイルを開くモード (rは読み取り用、wは書き込み用、aは追加用) です。
    • 何らかの理由でファイルを開けなかった場合に終了するチェックを追加します。
    • 文字列を取得したら、fprintf を使ってファイルに出力できます。
    • 最後に、fcloseを使用してファイルを閉じます。
  • これで、カンマ区切り値 (ミニスプレッドシートなど) の独自のCSVファイルをプログラムで作成できます。

グラフィックス

  • バイナリ形式で読み取り、ピクセルや色にマッピングして、画像やビデオを表示できます。ただし、イメージファイル内のビット数が限られている場合は、個々のピクセルが表示される前に拡大するしかありません。
    • しかし、人工知能と機械学習を使えば、他のデータに基づいて推測することで、以前にはなかった追加の詳細を生成できるアルゴリズムを使用できます。
  • ファイルを開き、それがJPEGファイルかどうか、特定の形式のイメージファイルかどうかを示すプログラムを見てみましょう。
#include <stdint.h>
#include <stdio.h>

typedef uint8_t BYTE;

int main(int argc, char *argv[])
{
    // Check usage
    if (argc != 2)
    {
        return 1;
    }

    // Open file
    FILE *file = fopen(argv[1], "r");
    if (!file)
    {
        return 1;
    }

    // Read first three bytes
    BYTE bytes[3];
    fread(bytes, sizeof(BYTE), 3, file);

    // Check first three bytes
    if (bytes[0] == 0xff && bytes[1] == 0xd8 && bytes[2] == 0xff)
    {
        printf("Maybe\n");
    }
    else
    {
        printf("No\n");
    }

    // Close file
    fclose(file);
}
  • 最初に、BYTE を8ビットとして定義し、Cでバイトをより簡単に型として参照できるようにします。
    • その後、ファイルをオープンし (実際にNULLでないファイルが返ってくることを確認します) 、fread を使ってファイルの最初の3バイトを bytes というバッファに読み込みます。
    • 最初の3バイト (16進数) とJPEGファイルの開始に必要な3バイトを比較できます。これらが同じ場合、ファイルはJPEGファイルである可能性が高くなります (他の種類のファイルでも、これらのバイトで始まる場合があります)。ただし、両者が同じでない場合は、JPEGファイルでないことは明らかです。
  • ファイルを自分で1バイトずつコピーすることもできます。
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>

typedef uint8_t BYTE;

int main(int argc, char *argv[])
{
    // Ensure proper usage
    if (argc != 3)
    {
        fprintf(stderr, "Usage: copy SOURCE DESTINATION\n");
        return 1;
    }

    // open input file
    FILE *source = fopen(argv[1], "r");
    if (source == NULL)
    {
        printf("Could not open %s.\n", argv[1]);
        return 1;
    }

    // Open output file
    FILE *destination = fopen(argv[2], "w");
    if (destination == NULL)
    {
        fclose(source);
        printf("Could not create %s.\n", argv[2]);
        return 1;
    }

    // Copy source to destination, one BYTE at a time
    BYTE buffer;
    while (fread(&buffer, sizeof(BYTE), 1, source))
    {
        fwrite(&buffer, sizeof(BYTE), 1, destination);
    }

    // Close files
    fclose(source);
    fclose(destination);
    return 0;
}
  • argv は引数を取得するために使用します。引数はファイル名として使用し、ファイルを開いて読み書きします。
    • その後、source ファイルから1バイトをバッファに読み込み、そのバイトを destination ファイルに書き込みます。while ループを使用してfreadを呼び出すことができ、読み込むバイトがなくなると fread は停止します。
  • これらの機能を使用して、ファイルの読み取りと書き込み、ファイルからのイメージのリカバリ、イメージ内のバイトを変更することによるイメージへのフィルタの追加を、今回の問題セットで行うことができます。