C言語の構造体のポインタの使い方

340, 2021-11-01

目次

C言語の構造体のポインタの使い方

C言語では構造体を扱うことができます。
もちろん構造体のポインタも使うことができます。
この記事ではC言語の構造体のポインタについて具体的に解説します。

C言語では構造体はポインタで扱うことが非常に多いです。
その方がメモリが省エネになり、速度も上がるからです。
C言語で構造体のポインタを扱えるようになるのは非常に有意義と言えます。

構造体のポインタは扱いかたがわかると簡単に扱うことができます。
それではC言語の構造体のポインタについて解説していきます。

構造体とは?

まずおさらいとして構造体とはなんなのか? というところから解説します。
構造体とは複数の変数をまとめた構造のことを言います。
たとえばAnimalという構造体があったとして、この構造体にeyesweightなどの変数を持たせます。

構造体からは構造体変数を定義することができます。
この構造体変数をアドレス演算子で参照すると、構造体変数のポインタが取り出せます。

たとえば先ほどの例のAnimalという構造体を宣言する場合は↓のようにします。

struct Animal {
    int eyes;
    double weight;
};

構造体変数を定義する場合はたとえば↓のようにします。

    struct Animal animal = { 2, 54.3 };

↑の場合、struct Animalが構造体でanimalが構造体変数です。
animaleyesには2, weightには54.3がセットされています。

ポインタとは?

ポインタもおさらいしておきます。
ポインタとは、変数や関数へのショートカットのことを言います。
ポインタを使うと変数や関数に間接的にアクセスすることができます。

ポインタ(アドレス)を格納する変数のことをポインタ変数と言います。
構造体でポインタを利用したい場合は、構造体変数にアドレス演算子を使うか、構造体のポインタ変数を使います。

ポインタ変数の宣言にはアスタリスク(*)を使います。
また、ポインタ変数の実体化(間接的な値の参照)でもアスタリスクが使われます。
構造体のポインタではこのアスタリスクに加えて、アロー演算子という演算子も使うことができます。

ポインタは変数や関数へのショートカットで、構造体の場合は構造体へのショートカットになります。

構造体のポインタのメリット・デメリット

構造体のポインタのメリット、デメリットはなんでしょうか?
構造体のポインタを使うことで得られる利点は?
また、気をつけたい点は?

これはまとめると↓になります。

  • ポインタを使えば省コストでメンバにアクセスできる

  • セグフォになる可能性がある

ポインタを使えば省コストでメンバにアクセスできる

メリットは、構造体のポインタを使えば省コストで構造体のメンバにアクセスできることです。

構造体のメンバのアクセスは、関連する処理によってコストがかかる場合があります。
たとえば関数に構造体変数を渡す場合を考えます。
無数のメンバを持つ構造体の構造体変数を関数の引数に渡すと、コピーが発生します。
そのコピーは構造体のメンバが多いほどコストが増えます。

しかし関数の引数に構造体のポインタを渡すようにした場合、そのコピーのコストはなくなります。
結果的に関数内で構造体のメンバにアクセスするときも、省コストでアクセスすることが出来るということになります。

このようにC言語の構造体のポインタを使えばコピーのコストを押さえてメンバにアクセスすることが出来ます。
関数と構造体を多用するとき、構造体のポインタは切っても切れない関係になります。

セグフォになる可能性がある

いっぽうデメリットは、構造体のポインタを使うとセグフォになってプログラムがクラッシュする場合があるということです。

これはポインタの特性によるものです。
ポインタを使う以上、セグフォになる可能性は捨てきれません。
これはポインタに[u]NULLポインタ[u/]や不正なアドレスが入っている場合があるからです。
構造体のポインタにおいてもこれはかわりません。

このようにC言語の構造体のポインタを使うと、セグフォになる可能性があります。
ですので注意が必要です。

構造体のポインタ変数の宣言方法

構造体のポインタ変数の宣言方法は↓になります。

構造体名 *構造体のポインタ変数名;

これを実際にコードにすると↓のようになります。

struct Animal {
    int eyes;
    double weight;
};

int main(void) {
    struct Animal *animal;  // ポインタ変数の宣言
    return 0;
}

構造体のポインタはこのように普通の変数のポインタと同じように宣言することができます。
宣言だけをした場合は構造体のポインタ変数にはデタラメな値(環境によって変わる)が入っています。
そのため宣言をしたら初期化をするか、あるいは宣言と同時に初期化するようにしたほうがいいです。
玄人のC言語使いは未初期化のポインタ変数を長いスパンで使うこともあります。

玄人のC言語使いはどこに生息しているの?

あまり表には出てこないよ

レア・モンスターみたいだな

構造体のポインタ変数の定義方法

構造体のポインタ変数を定義する場合は↓のようにします。

構造体名 *構造体のポインタ変数名 = 値;

宣言と定義の違いは実際に値が入っているか否かの違いです。
コードにすると↓のようになります。

#include <stdio.h>

struct Animal {
    int eyes;
    double weight;
};

int main(void) {
    struct Animal *animal = NULL;  // ポインタ変数の定義
    return 0;
}

↑のコードの場合、構造体Animalのポインタ変数animalにはNULLポインタが代入されています。
このようにポインタ変数をNULLポインタで定義するのは一般的でよく行われます。
とくに複雑なシビアな処理においては、バグを減らすためにこういった定義が行われることがあります。

また、構造体変数のアドレスを構造体のポインタ変数に代入しておく場合は↓のようにします。

struct Animal {
    int eyes;
    double weight;
};

int main(void) {
    struct Animal animal = {0};
    struct Animal *panimal = &animal;  // animalのアドレスを代入
    return 0;
}

↑の場合、Animalの構造体変数はanimal, Animalのポインタ変数はpanimalです。
さきにanimalを定義し、その後にpanimalanimalのアドレスを代入しています。

このようにすると構造体のポインタ変数を宣言と同時に他の変数のアドレスで初期化することができます。

構造体のメンバをポインタで参照する方法

構造体のメンバ変数にアクセスする方法どうしたらいいのでしょうか?
構造体のメンバ変数にアクセスする場合は、ドット演算子を使うのが普通でした。
ポインタ変数を使う場合はドット演算子の他にアロー演算子を使うことができます。

アロー演算子でメンバを参照する

ポインタ変数で構造体のメンバをアロー演算子で参照し、printf()で出力します。

#include <stdio.h>

struct Animal {
    int eyes;
    double weight;
};

int main(void) {
    struct Animal animal = { 2, 54.3 };
    struct Animal *panimal = &animal;

    printf("eyes[%d] weight[%f]\n", panimal->eyes, panimal->weight);
    // eyes[2] weight[54.300000]

    return 0;
}

↑の場合、Animal構造体のポインタ変数はpanimalです。
panimalにはAnimal構造体の構造体変数animalのアドレスが入っています。

panimal->eyesとやってアロー(->)を伸ばすと、間接的に構造体のポインタが指す実体のメンバにアクセスできます。
この例で言うと、panimalからanimalのメンバeyesを参照していことになります。
weightについても同様です。

このようにアロー演算子を使うと、構造体のポインタから実体のメンバにアクセスすることができます。

アロー演算子でメンバに代入する

ポインタ変数で構造体のメンバを参照して、メンバに値を代入します。

#include <stdio.h>

struct Animal {
    int eyes;
    double weight;
};

int main(void) {
    struct Animal animal = { 2, 54.3 };
    struct Animal *panimal = &animal;

    panimal->eyes = 4;
    panimal->weight = 123.4;

    printf("eyes[%d] weight[%f]\n", panimal->eyes, panimal->weight);
    // eyes[4] weight[123.400000]

    return 0;
}

↑の場合、Animal構造体の構造体変数animalのメンバはeyes2, weight54.3で初期化されています。
その後にポインタ変数panimalを通してアロー演算子でメンバに値を代入しています。
結果は代入した値が出力されます。

構造体のポインタを引数に取る関数

構造体のポインタを引数に取る関数を作りたい場合は↓のように関数を書きます。

#include <stdio.h>

struct Animal {
    int eyes;
    double weight;
};

// 構造体のポインタ変数を引数に取る関数
void func(struct Animal *panimal) {
    printf("eyes[%d]\n", panimal->eyes);
    printf("weight[%f]\n", panimal->weight);
}

int main(void) {
    struct Animal animal = { 2, 54.3 };

    func(&animal);  // アドレス(ポインタ)を渡す
    // eyes[2]
    // weight[54.300000]

    return 0;
}

構造体のポインタを関数で受け取る場合は、関数内で引数がNULLポインタかどうかチェックする場合があります。
セキュアなコードを書きたい場合は、そのようにチェックをした方がバグは減らせます。
その分手間は増えるのですが。

しかしそれでも不正なアドレスがポインタ変数に入っていた場合はセグフォなどになる可能性があります。
怖いのはセグフォにならずにプログラムが走ってしまうケースですが、このケースはバグの修正が困難になります。

構造体のポインタを返す関数

構造体のポインタを返す関数は↓のように書くことができます。

#include <stdio.h>

struct Animal {
    int eyes;
    double weight;
};

// 構造体のポインタを返す関数
struct Animal *func(struct Animal *panimal) {
    return panimal;
}

int main(void) {
    struct Animal animal = { 2, 54.3 };
    struct Animal *panimal = func(&animal);  // アドレス(ポインタ)を渡す
    return 0;
}

構造体のポインタを関数から返す場合、ローカル変数のポインタ変数を返すのはご法度です。
staticにして静的なメモリに確保している場合は別です。
ただそれでもスレッドセーフの問題などがあります。

関数内の構造体の変数は関数内で寿命が尽きます。
そのため、その変数のポインタを返しても意味がありません。

↑のコードの場合は関数の呼び出し時に渡している構造体変数animalのポインタをそのまま関数から返しています。
この場合はポインタの指し示す変数の寿命はmain関数内になるので安全と言えます。
ただこの関数に意味があるかといわれるとあまり意味はありません。

しかしC言語では関数が失敗したかどうかを表すのに構造体のポインタを返す場合があります。
NULLでなければ成功、NULLであれば失敗、という風にです。
そういう場合は↑のコードのように引数のポインタをそのままreturnする時があります。

constと構造体のポインタ

普通の構造体変数と同じように、構造体のポインタ変数にconstを付ければメンバを変更不能にできます。

struct Animal {
    int eyes;
    double weight;
};

int main(void) {
    struct Animal animal = {0};
    const struct Animal *panimal = &animal;  // animalのアドレスを代入

    panimal->eyes = 4;  // error!

    return 0;
}

構造体のポインタ変数とconstは相性が良いので積極的に使っていきたいところです。
constにしてメンバを保護すれば意図しない代入などを防ぐことができてバグを減らせます。
ただその分、設計の難易度は上がります。

constをまったく使わないというプロジェクトもあります。
プログラミング言語がそもそもconstをサポートしていないケースです。
そういった場合もほとんどの場合は問題になることはないようです。

ただ開発者のスキルが必要とされるので、チームの開発者のスキルにむらがある時constを使った設計をしたほうがいいかもしれません。

おわりに

今回はC言語の構造体のポインタを解説しました。
構造体のポインタを使えるようになると構造体を使うのが楽しくなってきます。
ぜひ押さえておきたい技術と言えます。

構造体を間接的に・・・

間接参照