構造体とポインタ

構造体のポインタ渡し

構造体は、そのまま関数の引数にして渡すことができます。
(構造体と関数の項参照)
この場合、関数に渡されるのは構造体変数をコピーしたものです。

構造体は複数のデータを一括して扱えますが、その分int型などに比べて構造体変数のデータサイズが大きくなりがちです。
関数の呼び出しの度に大きなデータのコピーが行われると、メモリ使用量や処理速度に影響が出ることも考えられます。

これを解決するのが、構造体をポインタで渡す方法です。
データをポインタで受け取る場合、コピーされるのは「アドレス」です。
これは32bit環境ならば4バイト、64bit環境では8バイトのデータです。
どれだけ巨大な構造体であろうとポインタのサイズは変わりませんから、コピーに掛かるコストは知れたものです。


#include <stdio.h>

#define NAME_LENGTH 50

typedef struct
{
    char name[NAME_LENGTH];
    int age;
    char gender;
} Person;

//構造体をポインタで受け取る関数
void PrintPerson(Person *p)
{
    printf("name: %s\n", (*p).name);
    printf("age: %d\n", (*p).age);
    printf("gender: %d\n", (*p).gender);
}

int main()
{
    Person person = { "A山B男", 20, 0 };
    PrintPerson(&person);

    getchar();
}

関数の仮引数には間接演算子(*)を付けて記述します。
呼び出し元(実引数)では、構造体変数にアドレス演算子(&)を付けて関数に渡します。

関数内では受け取ったポインタを元に処理を行うのですが、構造体のポインタ変数から各メンバ変数にアクセスするには、間接演算子ごと丸括弧で囲い、ドット演算子(.)でメンバ変数にアクセスします。 (15~17行目)

間接演算子とドット演算子とではドット演算子のほうが優先順位が高いので、丸括弧を記述しないと意味が変わってしまい、エラーになります。


//メンバ変数にアクセス
(*p).name

//これはNG
*p.name

//↑はこのような意味になってしまう
*(p.name)

アロー演算子

メンバ変数にアクセスする度に上記のような書き方をするのは面倒で、感覚的にもわかりにくいので、C言語では別の記述方法が用意されています。


#include <stdio.h>

#define NAME_LENGTH 50

typedef struct
{
    char name[NAME_LENGTH];
    int age;
    char gender;
} Person;

//構造体をポインタで受け取る関数
void PrintPerson(Person *p)
{
	//アロー演算子でメンバにアクセス
    printf("name: %s\n", p->name);
    printf("age: %d\n", p->age);
    printf("gender: %d\n", p->gender);
}

int main()
{
    Person person = { "A山B男", 20, 0 };
    PrintPerson(&person);

    getchar();
}

16~18行目以外は最初のサンプルコードと同じです。

構造体のポインタ変数のメンバにアクセスする方法が変わっています。
(*p).の代わりに、ハイフンと不等号を組み合わせてp->と記述することで、構造体ポインタ変数のメンバにアクセスすることができます。


//メンバ変数にアクセス
(*p).name

//↑と全く同じ意味
p->name

この演算子は矢印っぽいのでアロー演算子と呼ばれます。
(arrow=矢)
構造体のポインタ変数からメンバ変数にアクセスするにはアロー演算子を使用する、と覚えておきましょう。

関数内で変更されたくない場合

データをポインタで渡す都合上、関数内でデータを書き換えられてしまう恐れがあります。
関数内でデータを書き換えが起こらないことを保障するには引数にconstを指定します。


void PrintPerson(const Person *p)
{
	//コンパイル時エラーになる
	p->age = 30;
}

ポインタで高速代入

関数の引数に指定する場合と同様に、構造体変数に別の構造体変数を代入する場合、すべてのメンバ変数がコピーされます。
単純な記述方法で状態のコピーができるので便利ですが、やはり構造体のサイズが大きいと処理速度等に影響が出るおそれがあります。


#include <stdio.h>

#define NAME_LENGTH 50
#define PERSON_LENGTH 4

typedef struct
{
    char name[NAME_LENGTH];
    int age;
    char gender;
} Person;

//構造体Personの配列を年齢順に並び替える関数
void SortAge(Person arr[], int length)
{
    Person tmp; //一時保存用の変数

    for (int i = 0; i < length - 1; i++)
    {
        for (int j = length - 1; j > i; j--)
        {
            if (arr[j - 1].age > arr[j].age)
            {
                tmp = arr[j - 1];
                arr[j - 1] = arr[j];
                arr[j] = tmp;
            }
        }
    }
}

void PrintPerson(Person *p)
{
    printf("name: %s\n", p->name);
    printf("age: %d\n", p->age);
    printf("gender: %d\n", p->gender);
}

int main()
{
    Person person[] = {
        { "A山B男", 20, 0 },
        { "C下D太", 18, 0 },
        { "E田F子", 21, 1 },
        { "G山H美", 19, 1 },
    };

    SortAge(person, PERSON_LENGTH);

    for (int i = 0; i < PERSON_LENGTH; i++)
    {
        PrintPerson(&person[i]);
        printf("\n");
    }
    getchar();
}

関数SortAgeは、メンバ変数ageの値の小さい順に構造体配列を並べ替える関数です。
(配列と関数の項で登場した並べ替え関数を少し改造したものです)

関数内では、配列の各要素である構造体のコピーを繰り返すことで並べ替えを行っています。
このサンプルコードの場合は構造体のサイズも配列の要素数も大したことはありませんが、もっと大きな構造体を、もっと大量の要素数で用意した場合にこの並べ替え処理を行うと、そこそこ重たい処理となります。

これをポインタを利用して書き直すと以下のようになります。


#include <stdio.h>

#define NAME_LENGTH 50
#define PERSON_LENGTH 4

typedef struct
{
    char name[NAME_LENGTH];
    int age;
    char gender;
} Person;

//構造体Personの配列を年齢順に並び替える関数
void SortAge(Person **arr, int length)
{
    Person *tmp; //一時保存用の変数

    for (int i = 0; i < length - 1; i++)
    {
        for (int j = length - 1; j > i; j--)
        {
            if (arr[j - 1]->age > arr[j]->age)
            {
                tmp = arr[j - 1];
                arr[j - 1] = arr[j];
                arr[j] = tmp;
            }
        }
    }
}

void PrintPerson(Person *p)
{
    printf("name: %s\n", p->name);
    printf("age: %d\n", p->age);
    printf("gender: %d\n", p->gender);
}

int main()
{
    Person person[PERSON_LENGTH] = {
        { "A山B男", 20, 0 },
        { "C下D太", 18, 0 },
        { "E田F子", 21, 1 },
        { "G山H美", 19, 1 },
    };

    Person *personP[PERSON_LENGTH];
    for (int i = 0; i < PERSON_LENGTH; i++)
    {
        personP[i] = &person[i];
    }
    
    SortAge(personP, PERSON_LENGTH);

    for (int i = 0; i < PERSON_LENGTH; i++)
    {
        PrintPerson(personP[i]);
        printf("\n");
    }

    getchar();
}

まずはmain関数のほうから見ていきます。

48行目、ポインタ配列personPを宣言しています。
ポインタ配列PersonPには、あらかじめ構造体配列personの全ての要素のポインタを保存しておきます。
(49~52行目)

実際に並べ替えを行うのはポインタ配列personPのほうです。
関数内の処理はほとんど同じですが、並べ替えされているのは構造体変数ではなくそのポインタ変数です。
ポインタ変数は、どのようなデータ型を指していても情報量は4バイトです。
(64bit環境なら8バイト。ポインタと文字列を参照)
構造体Personがどれだけ巨大な構造体であったとしてもこれは変わりませんから、ループ文でコピーを繰り返しても大したコストにはなりません。
(ポインタ配列の宣言と初期化コストを考慮しても軽い)

なお、関数SortAgeの引数は、構造体変数の配列からポインタ変数の配列に変更しています。
「ポインタ」の「配列」の引数は、間接演算子を二つ並べて記述します。


//ポインタの配列
void SortAge(Person *arr[], int length)

//配列名は先頭要素へのポインタなのでこう書ける
void SortAge(Person **arr, int length)

//配列はポインタ渡しになるのと同じ
void test(int arr[])
//↓
void test(int *arr)

どっちでも同じことなので、好きな方で構いません。

さて、引数で受け取ったのは構造体のポインタの配列です。
値の一時保存のためのローカル変数を、構造体変数からポインタ変数に変更します。
(16行目)
ポインタからメンバにアクセスする場合はアロー演算子を使います。
(22行目)

最後に、関数SortAgeによって並び替えられたポインタ配列personPを使用して、結果を表示します。
(56~60行目)

name: C下D太
age: 18
gender: 0

name: G山H美
age: 19
gender: 1

name: A山B男
age: 20
gender: 0

name: E田F子
age: 21
gender: 1

Visual Studioのデバッグ機能で変数の中身を表示したのが以下です。
(アドレスは実行ごとに変わります)
ポインタを用いた並び替えの内部データ

構造体のポインタ配列personPageが小さい順に並び替えられているのに対して、元の構造体配列personは最初に宣言したまま変更されていないことがわかります。