プログラミング素人のはてなブログ

プログラミングも電気回路も専門外の技術屋の末端が勉強したことや作品をアウトプットするブログ。コードに間違いなど見つけられたら、気軽にコメントください。 C#、Python3、ラズパイなど。

ポインタと配列と構造体とメモリの切っても切れない関係(C言語)その2

ChatGPTにポインタと配列と構造体について教えてもらった

以下は、私がChatGPTとお話して教えてもらったことをまとめた内容になります。
私の知識から見ると、間違ったことは言っていないように思いますが、もし間違っていることがあったらぜひご指摘ください。

まずは基本の振り返りです。

構造体(Structures)

構造体は、複数の異なるデータ型を組み合わせて新しいデータ型を作るための仕組みです。
構造体はメンバ(Member)と呼ばれる変数を持ち、それぞれのメンバは異なるデータ型であることができます。
構造体の宣言には struct キーワードが使用され、メンバの定義は中括弧 {} 内に記述されます。
構造体の変数は通常、ドット演算子 . を使ってメンバにアクセスします。

配列(Arrays)

配列は、同じデータ型の要素が連続した領域に格納されるデータ構造です。
配列は固定サイズであり、要素のインデックスを使用して個々の要素にアクセスします。
配列は宣言時に要素のデータ型とサイズを指定します。

ポインタ(Pointers)

ポインタはメモリ上の別のデータを指し示す変数です。
ポインタはデータのメモリアドレスを保持します。
ポインタを使用することで、メモリ上のデータに対して直接アクセスしたり、データの共有や操作を行ったりすることができます。
ポインタは * を使って宣言され、アドレス演算子 & を使って変数のアドレスを取得します。
ポインタはドット演算子 . ではなく、アロー演算子 -> を使って構造体のメンバにアクセスします。
要点としては、構造体は異なるデータ型を組み合わせて新しいデータ型を作り、配列は同じデータ型の要素を連続して格納します。ポインタはメモリ上のデータを指し示し、直接アクセスや共有を可能にします。構造体や配列をポインタと組み合わせることで、データの操作や効率的なメモリ利用が可能になります。

以下、構造体を例にしたサンプルを載せていますが、配列でも同様のことが出来ます。

ポインタを使うメリットは?

ポインタを使用することによって、コンピュータのメモリを効率的に使用することができます。例えば、配列にアクセスする場合、配列全体を複製する必要がある場合があります。しかし、ポインタを使用することで、配列の各要素のアドレスを指すだけで、配列全体を複製する必要がなくなります。
また、ポインタを使用することで、関数に渡すデータの量を減らすことができます。例えば、配列を関数に渡す場合、配列全体を渡す必要がありますが、ポインタを使用することで、配列の先頭要素のアドレスを渡すだけで済みます。
さらに、ポインタを使用することで、動的なメモリ割り当てが可能になります。動的なメモリ割り当ては、プログラムの実行中に必要になったメモリを動的に確保することができます。動的なメモリ割り当てを行う場合、ポインタを使用して割り当てられたメモリブロックにアクセスする必要があります。

ポインタを使って構造体を関数に渡す処理

ポインタで構造体や配列を受け取った場合、この関数はサイズsizeof()を知ることはできません。
サイズが必要なときは、関数の外でサイズを取得し渡してあげる必要があります。

#include <stdio.h>

// 配列のポインタと要素数を受け取り、指定したインデックスに対応する値を取得する関数
int getValue(int* arr, int size, int index) {
    if (index >= 0 && index < size) {
        return arr[index];  // インデックスが有効範囲内の場合、指定した要素の値を返す
    } else {
        return -1;  // インデックスが無効範囲の場合、-1 を返す(適宜エラー処理に置き換えてください)
    }
}

int main() {
    int arr[] = {1, 2, 3, 4, 5};  // 配列の定義

    int index;

    printf("要素のインデックスを入力してください: ");
    scanf("%d", &index);

    int value = getValue(arr, sizeof(arr) / sizeof(arr[0]), index);

    if (value != -1) {
        printf("要素の値: %d\n", value);
    } else {
        printf("無効な範囲です\n");
    }

    return 0;
}

構造体のアドレスのポインタというのが度々出現します。
具体的には以下のようなものです。

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

typedef struct {
    char name[4];
    int age;
    char address[10];
} man;

int main(void) {
    int i;
    man man2;
    strcpy(man2.name, "bcde");
    man2.age = 54;
    strcpy(man2.address, "tokyo");
    
    /* 構造体の全要素の文字コードを出力する */
    unsigned char* ptr = (unsigned char*)&man2;
    for (i = 0; i < sizeof(man2); i++) {
        printf("%d ", ptr[i]);
    }
    
    return 0;
}

アドレスをポインタにキャストするというのは、ある変数やデータのメモリ上の場所を示すアドレスを、そのデータ型に対応するポインタ型に変換することです。
例えば、&man2man 型の変数 man2 のメモリ上の場所(アドレス)を表します。このアドレスを (unsigned char*) のようにキャストすると、そのアドレスを指すポインタ型として解釈します。
具体的には、(unsigned char*)&man2man型の変数 man2 の先頭アドレスを unsigned char* 型のポインタとして解釈します。これによって、man2 のメモリ上のバイト列に対して、1バイトずつアクセスすることが可能になります。
キャストを行うことで、プログラムはデータのバイト単位の内容にアクセスし、必要な操作(例えば、ASCIIコードの表示など)を行うことができます。
つまり、(unsigned char*)&man2man2 のメモリ上のバイト列へのポインタとして機能し、そのバイト列の内容を unsigned char 型として扱うことができるようになります。

構造体の参照渡し

参照渡しの関数では、実引数は&、仮引数は*であらわします。

#include <stdio.h>

typedef struct {
    int value;
} MyStruct;

void modifyStruct(MyStruct* ptr) {
    ptr->value = 42;
}

int main(void) {
    MyStruct myStruct;
    myStruct.value = 10;

    printf("Before: %d\n", myStruct.value);

    modifyStruct(&myStruct);

    printf("After: %d\n", myStruct.value);

    return 0;
}

C言語では、引数の渡し方には値渡し(pass-by-value)とポインタを使った参照渡し(pass-by-reference)があります。
*アスタリスク)は、引数の型の前に置かれることで、その引数がポインタであることを示します。ポインタは、メモリ上の別の場所を指し示す変数です。関数がポインタを受け取る場合、その関数内で引数が指し示すデータを直接変更することができます。これは参照渡しと呼ばれます。ポインタを使うことで、関数内での値の変更が呼び出し元にも反映されます。
&(アンド)は、仮引数の前に置かれることで、その仮引数が引数のアドレスを参照していることを示します。関数が引数を参照渡しで受け取る場合、関数の仮引数は引数のアドレスを受け取ります。これにより、関数内での値の変更が元の引数に反映されます。
つまり、ポインタを使用することで関数内での値の変更が元の引数に影響を与えることができます。一方、参照渡しをするためには、関数の仮引数が引数のアドレスを受け取る必要があります。

構造体において"."と"->"の違いは何?

.(ドット)演算子は、構造体のメンバに直接アクセスするために使用されます。構造体の変数名の後にドット演算子を付け、その後にメンバ名を指定します。例えば、struct_name.member_name のような形式で使用します。
一方、->(アロー)演算子は、ポインタを介して構造体のメンバにアクセスするために使用されます。ポインタの指す先の構造体のメンバにアクセスするために、ポインタ名の後にアロー演算子を付け、その後にメンバ名を指定します。例えば、pointer->member_name のような形式で使用します。

ドット演算子とアロー演算子は、通常の構造体の場合、それぞれ構造体変数と構造体ポインタを介してメンバにアクセスするために使用されます。したがって、通常の場合においてはドット演算子とアロー演算子は置換可能ではありません。
ただし、以下のような場合に限ってドット演算子とアロー演算子を交換できます:
構造体のメンバがポインタである場合: もし構造体のメンバがポインタであり、そのポインタが有効なメモリ領域を指している場合、ドット演算子ではなくアロー演算子を使ってポインタが指す先のメンバにアクセスすることができます。これにより、ポインタの解参照を省略できます。
構造体自体がポインタである場合: もし構造体そのものがポインタであり、そのポインタが有効なメモリ領域を指している場合、アロー演算子ではなくドット演算子を使って構造体のメンバにアクセスすることができます。これにより、ポインタの解参照を省略できます。

これは以下のような使い方をしたときになります。

#include <stdio.h>

typedef struct {
    int x;
    int y;
} Point;

int main() {
    Point point1;
    point1.x = 5;
    point1.y = 10;

    Point* point2_ptr = &point1;
    (*point2_ptr).x = 15;
    (*point2_ptr).y = 20;

    printf("ドット演算子: (%d, %d)\n", point1.x, point1.y);
    printf("アロー演算子: (%d, %d)\n", point2_ptr->x, point2_ptr->y);

    return 0;
}

上記の例では、Point という名前の構造体を定義し、point1 という構造体変数と point2_ptr という構造体ポインタ変数を宣言しています。
point1 にはドット演算子を使用して直接アクセスし、point1.x や point1.y に値を代入しています。
point2_ptr は point1 のアドレスを指しており、アロー演算子を使用して point2_ptr->x や point2_ptr->y に値を代入しています。
ポインタが有効なメモリ領域を指している場合、ドット演算子とアロー演算子を交換してもポインタの指すメモリ領域が無効な場合、ドット演算子とアロー演算子を交換しても正しい結果が得られません。ポインタが有効なメモリ領域を指しているかどうかを確認せずに交換してしまうと、実行時エラーや未定義の動作が発生する可能性があります。
アロー演算子は、ポインタが指す先のメモリ領域が構造体であることを保証するための安全性を持っています。ポインタの指す先が有効な構造体である場合、アロー演算子を使用してメンバにアクセスすることができます。ドット演算子はポインタの指す先が構造体であることを前提とせず、直接的に構造体変数に対して使用されます。
したがって、ポインタが有効なメモリ領域を指している場合にのみ、ドット演算子とアロー演算子を交換して使用することが適切です。ポインタが有効なメモリ領域を指しているかどうかを適切に確認し、安全な操作を行うようにしてください。

異なる構造体を受け取るジェネリックな関数

ジェネリックな関数として実装し、void*型のポインタとして受け取ることで異なる構造体を受け取ることが出来ます。
ジェネリックとは、プログラミング言語の機能・仕様の一つで、同じコードで様々な異なるデータ型のデータを処理できるようにする仕組みのことです。
また、書き換えたものはポインタを返す通して返すことが出来ます。

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

typedef struct {
    int id;
    char name[20];
} Person;

typedef struct {
    int id;
    char name[20];
    int age;
} Person1;

void createPerson(void* ptr, int c) {
    if (c == 0) {
        Person* person = (Person*)ptr;
        person->id = 1;
        strncpy(person->name, "John Doe", sizeof(person->name));
    } else {
        Person1* person = (Person1*)ptr;
        person->id = 10;
        strncpy(person->name, "John", sizeof(person->name));
        person->age = 20;
    }
}

int main() {
    Person person;
    createPerson(&person, 0);
    printf("Person ID: %d\n", person.id);
    printf("Person Name: %s\n", person.name);

    Person1 person1;
    createPerson(&person1, 1);
    printf("Person ID: %d\n", person1.id);
    printf("Person Name: %s\n", person1.name);
    printf("Person Age: %d\n", person1.age);

    void* ptr;

    // Person構造体を受け取る
    ptr = &person;
    createPerson(ptr, 0);
    printf("Person ID: %d\n", person.id);
    printf("Person Name: %s\n", person.name);

    // Person1構造体を受け取る
    ptr = &person1;
    createPerson(ptr, 1);
    printf("Person ID: %d\n", person1.id);
    printf("Person Name: %s\n", person1.name);
    printf("Person Age: %d\n", person1.age);

    return 0;
}

まとめ

ChatGPTとポインタと配列と構造体についてお話しました。
このブログ記事のコードはすべてChatGPTが出力したものです。
ChatGPTが出力するコードは、まれにエラーになるコードがありますが、実行して確認することでこのような間違いはほぼ防ぐことが出来ます。
これまで、GAS(Google app script)やPython3、GoなどでもChatGPTを使ってみましたが、比較的新しいライブラリ等が関係しない話であれば、ほぼ正しいコードを出力しているように見えます。
また、私自身がポインタに関して何となく使えてはいるけどなんでこう書くの?といわれて説明できなかった部分や、なんでこういうコードになるの?と思っていいた部分もChatGPTの解説でわかったような気がします。