見出し画像

C言語学習4

昨日は苦C 17章から19章をやりました。
ポインタの理解が曖昧でなかなkコンパイルできませんでした。

番号	名前	テストの平均点
1	野比のび太	0
2	源静香	90
3	剛田武	40
4	骨川スネ夫	7

練習問題17
上記の表をエクセルでも開ける形式で作成するプログラムを作成せよ。
ヒント:CSV形式はどんな表計算ソフトでも開ける。

苦C 17章

解答1

#include <stdio.h>
#include <string.h>

typedef struct {
    char data[128];
} doraemons;

int main(void) {
    doraemons character[5];
    strcpy(character[0].data, "番号, 名前, テストの平均点\n");
    strcpy(character[1].data, "1, 野比のび太, 0\n");
    strcpy(character[2].data, "2, 源静香, 90\n");
    strcpy(character[3].data, "3, 剛田武, 40\n");
    strcpy(character[4].data, "4, 骨川スネ夫, 7\n");

    FILE *file;
    file = fopen("test.txt", "w");
    if (file != NULL) {
        for (int i = 0; i < 5; i++) {
            fprintf(file, "%s", character[i].data);
        }
        fclose(file);
    } else {
        printf("ファイルを開けませんでした。\n");
    }

    return 0;
}

答え

#include <stdio.h>

int main(void)
{
    FILE* fp;

    fp = fopen("test.csv", "w");

    fprintf(fp, "番号,名前,テストの平均点\n");
    fprintf(fp, "1,野比のび太,0\n");
    fprintf(fp, "2,源静香,90\n");
    fprintf(fp, "3,剛田武,40\n");
    fprintf(fp, "4,骨川スネ夫,70,\n");

    fclose(fp);

    return 0;
}

解答2

#include <stdio.h>
#include <string.h>

typedef struct {
    int number;
    char name[64];
    int meanTestScore;
    char data[128];

} doraemons;

int main(void) {
    doraemons character[5];

    strcpy(character[0].data, "番号, 名前, テストの平均点\n");

    character[1].number = 1;
    strcpy(character[1].name, "野比のび太");
    character[1].meanTestScore = 0;

    character[2].number = 2;
    strcpy(character[2].name, "源静香");
    character[2].meanTestScore = 90;

    character[3].number = 3;
    strcpy(character[3].name, "剛田武");
    character[3].meanTestScore = 40;

    character[4].number = 4;
    strcpy(character[4].name, "骨川スネ夫");
    character[4].meanTestScore = 70;

    int charactersCount = sizeof(character) / sizeof(character[0]);
    for (int i = 1; i < charactersCount; i++) {
        sprintf(character[i].data, "%d, %s, %d\n", character[i].number,
                character[i].name, character[i].meanTestScore);
    }

    FILE *file;
    file = fopen("doraemonCharacters.csv", "w");
    if (file != NULL) {
        for (int i = 0; i < 5; i++) {
            fprintf(file, "%s", character[i].data);
        }
        fclose(file);

    } else {
        printf("ファイルを開けませんでした。\n");
    }

    return 0;
}

chatGPTにリファクタリングしてもらった。

リファクタリングを行うことで、コードの可読性と効率性を向上させることができます。以下のポイントに注意してリファクタリングを行います:構造体定義の見直し: data フィールドは不要です。numbernamemeanTestScore を直接使用してファイルに出力します。
データの初期化方法の改善: 構造体の配列を初期化する際、各フィールドを個別に設定するのではなく、初期化リストを使用します。
ファイル出力処理の簡略化: sprintf を使用して data にフォーマットされた文字列を格納する代わりに、直接 fprintf でファイルに出力します。

修正後のコードは以下のようになります:

GPT-4
#include <stdio.h>
#include <string.h>

typedef struct {
    int number;
    char name[64];
    int meanTestScore;
} doraemons;

int main(void) {
    doraemons character[5] = {
        {0, "", 0},  // ヘッダー行のためのダミーデータ
        {1, "野比のび太", 0},
        {2, "源静香", 90},
        {3, "剛田武", 40},
        {4, "骨川スネ夫", 70}
    };
    FILE *file = fopen("doraemonCharacters.csv", "w");
    if (file != NULL) {
        fprintf(file, "番号, 名前, テストの平均点\n");
        for (int i = 1; i < 5; i++) {
            fprintf(file, "%d, %s, %d\n", character[i].number,
                    character[i].name, character[i].meanTestScore);
        }
        fclose(file);
    } else {
        printf("ファイルを開けませんでした。\n");
    }

    return 0;
}

めちゃめちゃ短くなって笑った。
構造体の初期化ってブロック使って一気に全要素いけるんだな〜!
すごい!めっちゃ楽じゃん!

リファクタリングしてもらうと知らないこと教えてくれるから、これからコード書いたら毎回聞いてみようと思いました。


練習問題18
練習問題11で作成した、
「西暦年を入力するとオリンピックが開かれるか表示するプログラムを作成せよ。
ただし、オリンピックの開催を計算する部分は別の関数として作成せよ。」
という問題を元に、さらに関数の戻り値をenum定数で返すようにしなさい。

苦C 18章

解答

#include <stdio.h>
#include <string.h>

enum {
    nonOlimpicYear,
    summerOlympicYear,
    winterOlympicYear,
};

int findOlympicYear(int year);

int main(void) {
    int year;
    int isOlympicYear;
    char message[128];
    printf("西暦を入力してください: ");
    scanf("%4d", &year);
    isOlympicYear = findOlympicYear(year);
    if (isOlympicYear == nonOlimpicYear) {
        sprintf(message, "西暦%d年は、オリンピック開催年ではありません。",
                year);
    } else if (isOlympicYear == summerOlympicYear) {
        sprintf(message, "西暦%d年は、夏季オリンピック開催年です。", year);

    } else if (isOlympicYear == winterOlympicYear) {
        sprintf(message, "西暦%d年は、冬季オリンピック開催年です。", year);

    } else {
        sprintf(message, "エラーです。");
    }

    printf("%s", message);

    return 0;
}

int findOlympicYear(int year) {
    int result;
    if (year % 4 == 0) {
        result = summerOlympicYear;

    } else if (year % 2 == 0 && year % 4 != 0) {
        result = winterOlympicYear;

    } else {
        result = nonOlimpicYear;
    }

    return result;
}

答え

#include <stdio.h>

int olympic(int year);

enum {
    OLYMIPC_NON,
    OLYMIPC_SUMMER,
    OLYMIPC_WINTER,
};

int main(void)
{
    int year, hold;

    scanf("%d", &year);
    hold = olympic(year);

    switch (hold) {
    case OLYMIPC_NON:
        printf("開かれない\n");
        break;
    case OLYMIPC_SUMMER:
        printf("夏季五輪\n");
        break;
    case OLYMIPC_WINTER:
        printf("冬季五輪\n");
        break;
    };

    return 0;
}

int olympic(int year)
{
    if (year % 2 == 0) {
        if (year % 4 == 0) {
            return OLYMIPC_SUMMER;
        } else {
            return OLYMIPC_WINTER;
        }
    } else {
        return OLYMIPC_NON;
    }
}

chatGPTのアドバイス

あなたの解答と問題の答えを比較して、いくつかのアドバイスとリファクタリングの提案をします。

あなたの解答について
あなたの解答は問題の要件を満たしており、enumを使ってオリンピックの開催年を表現しています。いくつかの改善点は以下の通りです:Enumの命名:
Enumの要素名が少し誤解を招く可能性があります。例えば、nonOlimpicYearNonOlympicYear とするほうが一般的な英語の綴りに合致します。
エラーチェックの欠如:
scanfでの入力時にエラーチェックを行うことで、不正な入力(非数値など)を処理できます。

問題の答えについて
問題の答えは、switch文を使ってenumの値に基づいて異なる出力を行っています。これはコードを簡潔にし、可読性を高める良い方法です。

リファクタリングの提案Enumの命名の改善: Enumの要素名を分かりやすくします。
エラーチェックの追加: scanfの後にエラーチェックを追加します。
switch文の使用: if-else 文の代わりに switch 文を使用してコードの可読性を向上させます。

修正後のコードは以下の通りです:

#include <stdio.h>

enum OlympicType {
    NonOlympicYear,
    SummerOlympicYear,
    WinterOlympicYear
};

int findOlympicYear(int year);

int main(void) {
    int year, result;
    printf("西暦を入力してください: ");
    if (scanf("%4d", &year) != 1) {
        printf("無効な入力です。\n");
        return 1;
    }

    result = findOlympicYear(year);

    switch (result) {
        case SummerOlympicYear:
            printf("西暦%d年は、夏季オリンピック開催年です。\n", year);
            break;
        case WinterOlympicYear:
            printf("西暦%d年は、冬季オリンピック開催年です。\n", year);
            break;
        case NonOlympicYear:
        default:
            printf("西暦%d年は、オリンピック開催年ではありません。\n", year);
            break;
    }

    return 0;
}

int findOlympicYear(int year) {
    if (year % 4 == 0) {
        return SummerOlympicYear;
    } else if (year % 2 == 0) {
        return WinterOlympicYear;
    } else {
        return NonOlympicYear;
    }
}

このリファクタリングでは、コードがより簡潔で理解しやすくなり、ユーザー入力のエラーチェックが追加されています。また、switch文を

使用することで、各オリンピックタイプに対する処理が明確に区別され、コードの可読性が向上しています。enum の命名もより一般的な英語のスペルに従い、意図がより明確に伝わるようになりました。

全体として、あなたの解答は問題の要件を適切に満たしていましたが、上記のリファクタリングにより、プログラムの堅牢性と可読性がさらに向上することでしょう。また、ユーザー入力のエラーハンドリングは、実際のプログラムにおいて非常に重要な部分です。ユーザーからの不適切な入力に適切に対処することで、プログラムの安全性と信頼性を保つことができます。

GPT-4

アドバイスとリファクタリングを基に修正。
enum定数を大文字にしただけです。

#include <stdio.h>
#include <string.h>

enum {
    OLYMPIC_NON,
    OLYMPIC_SUMMER,
    OLYMPIC_WINTER,
};

int findOlympicYear(int year);

int main(void) {
    int year, result;
    printf("西暦を入力してください: ");
    if (scanf("%4d", &year) != 1) {
        printf("無効な入力です。\n");
        return 1;
    }

    result = findOlympicYear(year);
    switch (result) {
        case OLYMPIC_SUMMER:
            printf("西暦%d年は、夏季オリンピック開催年です。", year);
            break;

        case OLYMPIC_WINTER:
            printf("西暦%d年は、冬季オリンピック開催年です。", year);
            break;

        case OLYMPIC_NON:
            printf("西暦%d年は、オリンピック開催年ではありません。", year);
            break;
    }

    return 0;
}

int findOlympicYear(int year) {
    if (year % 4 == 0) {
        return OLYMPIC_SUMMER;

    } else if (year % 2 == 0) {
        return OLYMPIC_WINTER;

    } else {
        return OLYMPIC_NON;
    }
}


練習問題16の、
「3人分の、名前、年齢、性別、を入力して表示するプログラムを作りなさい。
ただし、データは構造体で記憶することとし、
また、データの入力と表示はそれぞれ専用の関数を作って行うこととする。」
という問題を元に、何人分でも入力できるように改造しなさい。
なお、年齢に-1が入力されれば入力終了とする。
※配列番号がint型なのでint型の最大値まで扱えれば良い。

苦C 19章

解答

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
    char name[64];
    int age;
    int sex;
} people;

#define MAN "男性"
#define WOMEN "女性"
#define OTHER "その他"

int inputPeople(people *data, int i);
int printPeople(people *data, int i);

int main(void) {
    people *data;
    int dataElements = 10;
    data = (people *)malloc(sizeof(people) * dataElements);

    int i = 0;
    do {
        inputPeople(&data[i], i);
        i++;
        if (dataElements == i) {
            dataElements += 10;
            data = (people *)realloc(data, sizeof(people) * dataElements);
        }

    } while (data[i - 1].age != -1);

    int count = i;
    for (i = 0; i < count; i++) {
        printPeople(&data[i], i);
    }

    free(data);
    return 0;
}

int inputPeople(people *data, int i) {
    printf("%d人目のデータを入力してください。\n", i + 1);
    printf("名前: ");
    scanf("%63s", data->name);

    printf("年齢: ");
    scanf("%3d", &data->age);

    printf("性別 1:男性, 2:女性, 3,その他: ");
    scanf("%1d", &data->sex);

    return 0;
}

int printPeople(people *data, int i) {
    char sex[64];
    switch (data->sex) {
        case 1:
            strcpy(sex, MAN);
            break;
        case 2:
            strcpy(sex, WOMEN);
            break;
        default:
            strcpy(sex, OTHER);
            break;
    }
    printf("%03d人目\t名前: %6s,\t年齢: %3d,\t性別: %2s\n", i + 1, data->name,
           data->age, sex);

    return 0;
}

答え

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
    char name[256];
    int age;
    int sex;
} People;

void InputPeople(People* data);
void ShowPeople(People data);

int main(void)
{
    int i, count, datasize;
    People* data;

    datasize = 10;
    data = (People*)malloc(sizeof(People) * datasize);

    count = 0;

    while (1) {
        InputPeople(&data[count]);
        if (data[count].age == -1)
            break;
        count++;

        if (count >= datasize) {
            datasize += 10;
            data = (People*)realloc(data, sizeof(People) * datasize);
        }
    }

    for (i = 0; i < count; i++) {
        ShowPeople(data[i]);
    }

    free(data);

    return 0;
}

void InputPeople(People* data)
{
    printf("名前:");
    scanf("%s", data->name);
    printf("年齢:");
    scanf("%d", &data->age);
    printf("性別(1-男性、2-女性):");
    scanf("%d", &data->sex);
    printf("\n");
}

void ShowPeople(People data)
{
    char sex[16];

    printf("名前:%s\n", data.name);
    printf("年齢:%d\n", data.age);

    if (data.sex == 1) {
        strcpy(sex, "男性");
    } else {
        strcpy(sex, "女性");
    }

    printf("性別:%s\n", sex);
    printf("\n");
}

chatGPTのアドバイス

あなたの解答と問題の答えを比較してみると、いくつかの違いと改善点が見受けられます。それぞれについてアドバイスとリファクタリングを行います。

あなたの解答についてのアドバイス
メモリ再割り当てのタイミング:
あなたのコードでは、dataElements == i でメモリ再割り当てを行っていますが、この条件式は、配列の要素がすべて埋まった後に拡張を行っています。理想的には、新しいデータを追加する前に拡張を行うべきです。
無限ループの使用:
問題の答えでは while (1) を使って無限ループを作成し、ループ内で入力終了の条件をチェックしています。これは、終了条件が複数ある場合や、条件が複雑な場合に便利です。
関数の戻り値:
inputPeople と printPeople 関数は常に 0 を返しています。戻り値が不要な場合、これらの関数を void 型にすると良いでしょう。
リファクタリングの提案
メモリ再割り当てのロジックを改善し、新しいデータを追加する前に配列を拡張します。
while (1) を使用し、終了条件をループ内でチェックします。
不要な戻り値がある場合は、関数を void 型にします。
修正案の例を以下に示します:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
    char name[64];
    int age;
    int sex;
} People;

void InputPeople(People *data);
void ShowPeople(People data);

int main(void) {
    int count = 0, dataSize = 10;
    People *data = malloc(sizeof(People) * dataSize);

    while (1) {
        if (count >= dataSize) {
            dataSize += 10;
            data = realloc(data, sizeof(People) * dataSize);
        }

        InputPeople(&data[count]);
        if (data[count].age == -1) break;
        count++;
    }

    for (int i = 0; i < count; i++) {
        ShowPeople(data[i]);
    }

    free(data);
    return 0;
}

void InputPeople(People *data) {
    printf("名前: ");
    scanf("%63s", data->name);
    printf("年齢: ");
    scanf("%d", &data->age);
    if (data->age == -1) return;
    printf("性別 (1-男性、2-女性、その他): ");
    scanf("%d", &data->sex);
}

void ShowPeople(People data) {
    char sex[16];
    switch (data.sex) {
        case 1:
            strcpy(sex, "男性");
            break;
        case 2:
            strcpy(sex, "女性");
            break;
        default:
            strcpy(sex, "その他");
            break;
    }

    printf("%d人目\t名前: %s,\t年齢: %d,\t性別: %s\n", data.name, data.age,
           sex);
}

このリファクタリングでは、次の点を改善しています:
- 新しいデータを追加する前に配列を拡張するようにしています。
- `while (1)` を用いて無限ループを作成し、ループ内で終了条件をチェックしています。これにより、コードがよりシンプルかつ直感的になります。
- `InputPeople` 関数の中で年齢が `-1` の場合にすぐにリターンするようにしました。これにより、年齢が `-1` の場合に不要な性別の入力を求めることを避けています。
- `inputPeople` と `printPeople` 関数の戻り値を `void` に変更しています。これは、これらの関数が成功を示す値を返す必要がないためです。

これらの変更により、プログラムはより効率的かつ堅牢になり、使いやすくなっています。

GPT-4

do-while内でのif条件がよくないの気づいてなかった。
ループ文もdo-whileじゃなくてwhileで無限ループを作った方が条件がわかりやすくて読みやすい。
なんで ShowPeople関数の引数が People *dataじゃないのか聞いてみた。

`ShowPeople` 関数が `data` をポインタではなく、値(`People data`)として受け取っている理由は、関数内で構造体のコピーを作成するためです。このアプローチにはいくつかの理由と利点があります。

1. **変更の防止**: 関数に構造体のポインタを渡すと、その関数内で構造体の内容を変更する可能性があります。値として渡すことで、元の構造体データへの不用意な変更を防止できます。

2. **読みやすさと単純さ**: 構造体を値として渡すことで、関数の引数が単純化され、関数内でのデータ操作がより読みやすくなります。特に、構造体が小さい場合やパフォーマンスが重要でない場合に適しています。

3. **スレッドセーフティ**: 複数のスレッドが同じデータにアクセスする場合、ポインタを介してデータを共有すると競合が発生する可能性があります。値として渡すことで、各スレッドにデータのコピーが与えられ、この問題を回避できます。

ただし、この方法には欠点もあります。構造体が大きい場合、値として渡すことで無駄なメモリコピーが発生し、パフォーマンスに影響を与える可能性があります。また、構造体の内容を変更して呼び出し元に反映させたい場合は、ポインタを使用する必要があります。

プログラミングにおいては、これらのトレードオフを理解し、状況に応じて適切な方法を選択することが重要です。

GPT-4

ShowPeople関数は構造体の値を出力するだけだから、変更されないようにした方が確かに安全だな〜。
アドバイスをもらってリファクタリングしてもらうと、
どのように改善していけばいいか明確にわかるのでとてもいい!

これから20章をやって、終わったらC言語以外の学習をしようと思います。

この記事が気に入ったらサポートをしてみませんか?