6日でできる 新C言語入門|構造体ポインタの基本と使い方

構造体をコピーせず、必要な場所だけを指して操作する。
構造体ポインタを理解して、C言語のデータ受け渡しをもっと効率よく扱おう。

C言語の構造体は、複数の関連するデータを1つにまとめて扱うための便利な仕組みです。商品データ、学生データ、センサー情報、成績データのように、複数の項目を1つのまとまりとして管理したい場面でよく使われます。

そして、構造体をさらに実用的に扱うために重要になるのが構造体ポインタです。

構造体ポインタは、構造体変数のアドレスを保存するポインタです。通常のint型変数にint*のポインタを使えるように、構造体変数にも構造体型のポインタを使えます。

構造体そのものを関数に渡すと、構造体の内容がコピーされます。小さな構造体なら問題になりにくいですが、メンバが多い構造体や大きな配列を含む構造体では、コピーの負担が大きくなることがあります。

また、関数内で構造体の値を変更しても、値渡しの場合は呼び出し元の構造体には反映されません。一方、構造体のアドレスを渡すと、関数内から呼び出し元の構造体を直接操作できます。

この記事では、構造体ポインタの基本、->演算子の使い方、値渡しとポインタ渡しの違い、アドレスの確認、構造体配列とポインタ演算の関係まで、サンプルプログラムと図を使ってやさしく解説していきます。

構造体ポインタとは何か

構造体ポインタは、構造体変数のアドレスを保存するためのポインタ変数です。

たとえば、BOOKという構造体型があるとします。

typedef struct {
    int id;
    char title[100];
    double price;
} BOOK;

このBOOK型の変数を作ると、次のように書けます。

BOOK book;

そして、このbookのアドレスを保存するポインタは次のように宣言できます。

BOOK* pBook;

pBookは、BOOK型の構造体を指すポインタです。

構造体変数bookのアドレスを代入するには、&を使います。

pBook = &book;
書き方意味
BOOK bookBOOK型の構造体変数bookを宣言する
BOOK* pBookBOOK型の構造体を指すポインタを宣言する
pBook = &bookbookのアドレスをpBookに代入する
pBook->idpBookが指す構造体のidメンバにアクセスする

構造体ポインタを使うと、構造体変数そのものをコピーせず、アドレスを通して構造体の中身を扱えます。

構造体変数は.、構造体ポインタは->でアクセスする

構造体変数のメンバにアクセスするときは、ドット演算子を使います。

book.id
book.title
book.price

一方、構造体ポインタが指す先のメンバにアクセスするときは、アロー演算子を使います。

pBook->id
pBook->title
pBook->price

pBook->idは、次の書き方と同じ意味です。

(*pBook).id

ただし、(*pBook).idは少し読みにくいため、構造体ポインタではpBook->idの形がよく使われます。

対象メンバアクセスの書き方
構造体変数bookbook.id
構造体ポインタpBookpBook->id
構造体ポインタを明示的に参照(*pBook).id

ここで大切なのは、pBookは構造体そのものではなく、構造体のアドレスを持っているということです。
そのため、pBook.idとは書けません。pBookが指している先のメンバにアクセスするため、pBook->idと書きます。

図:構造体変数と構造体ポインタの関係

この図から分かること

この図から分かるのは、構造体変数と構造体ポインタでは、メンバにアクセスする書き方が異なるということです。

bookのような構造体変数から直接メンバを見る場合はドット演算子を使います。pBookのような構造体ポインタから、指している構造体のメンバを見る場合はアロー演算子を使います。構造体ポインタは構造体のアドレスを持っているため、->を使って指す先のメンバへアクセスします。

構造体を関数に渡す2つの方法

構造体を関数に渡す方法には、大きく分けて値渡しとポインタ渡しがあります。

値渡しでは、構造体の内容が関数側にコピーされます。関数内でコピーを変更しても、呼び出し元の構造体には影響しません。

ポインタ渡しでは、構造体のアドレスを関数に渡します。関数側では、そのアドレスを通して呼び出し元の構造体を操作できます。

渡し方関数に渡るもの関数内の変更特徴
値渡し構造体のコピー呼び出し元に反映されない元データを守りやすいがコピーが発生する
ポインタ渡し構造体のアドレス呼び出し元に反映されるコピーを避けられ、元データを直接操作できる

構造体の内容を表示するだけなら、値渡しでも動作します。
ただし、構造体が大きい場合や、関数内で元のデータを変更したい場合は、ポインタ渡しがよく使われます。

値渡しとポインタ渡しの違いを確認する

次のプログラムでは、構造体を値渡しした場合と、ポインタ渡しした場合の違いを確認します。

プロジェクト/ファイル名: Lesson65_1/main.c

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

typedef struct {
    int code;
    char title[100];
    double price;
} BOOK;

// 値渡し:構造体のコピーを受け取る
void updateByValue(BOOK book) {
    book.code = 2;
    strcpy(book.title, "C言語実践編");
    book.price = 2800.0;
}

// ポインタ渡し:構造体のアドレスを受け取る
void updateByPointer(BOOK* pBook) {
    pBook->code = 2;
    strcpy(pBook->title, "C言語実践編");
    pBook->price = 2800.0;
}

int main(void) {
    BOOK book1 = { 1, "C言語入門編", 1800.0 };
    BOOK book2 = { 1, "C言語入門編", 1800.0 };

    // 値渡しで関数を呼び出す
    updateByValue(book1);

    // ポインタ渡しで関数を呼び出す
    updateByPointer(&book2);

    printf("book1: %d %s %.1f\n", book1.code, book1.title, book1.price);
    printf("book2: %d %s %.1f\n", book2.code, book2.title, book2.price);

    return 0;
}

実行結果

book1: 1 C言語入門編 1800.0
book2: 2 C言語実践編 2800.0

book1は値渡しで関数に渡されています。

updateByValue(book1);

値渡しでは、book1のコピーがupdateByValue関数に渡されます。関数内でbook.codeやbook.titleを変更しても、それはコピーに対する変更です。そのため、main関数側のbook1は変わりません。

一方、book2はアドレスを渡しています。

updateByPointer(&book2);

updateByPointer関数では、BOOK* pBookとしてアドレスを受け取っています。

void updateByPointer(BOOK* pBook)

関数内では、pBook->code、pBook->title、pBook->priceのように、ポインタが指す先の構造体を変更しています。

そのため、main関数側のbook2にも変更が反映されます。

値渡しではコピーが作られる

値渡しでは、関数に構造体のコピーが渡されます。

つまり、関数内で変更しているのは、呼び出し元の構造体そのものではありません。

イメージとしては、次のようになります。

場所対象変更の影響
main関数book1元の構造体
updateByValue関数bookbook1のコピー
updateByValue内の変更コピーだけが変わるbook1には反映されない

この性質は、元のデータを変更したくない場合には安全です。
ただし、大きな構造体ではコピーのコストが気になる場合があります。

ポインタ渡しでは元の構造体を操作できる

ポインタ渡しでは、構造体のアドレスを関数に渡します。

関数側では、そのアドレスを使って呼び出し元の構造体を操作します。

場所対象変更の影響
main関数book2元の構造体
updateByPointer関数pBookbook2のアドレス
updateByPointer内の変更pBookが指す先を変更book2に反映される

ポインタ渡しは、次のような場面でよく使います。

場面理由
関数内で構造体を変更したい呼び出し元のデータを直接更新できる
構造体が大きいコピーを避けられる
複数の値をまとめて更新したい構造体のメンバをまとめて扱える
配列や動的メモリと組み合わせたい先頭アドレスを使って連続データを扱える

構造体ポインタを使うと、効率と柔軟性の両方を得やすくなります。

図:値渡しとポインタ渡しの違い

この図から分かること

この図から分かるのは、値渡しとポインタ渡しでは、関数内で操作している対象が異なるということです。

値渡しでは、関数内に構造体のコピーが作られます。そのコピーを変更しても、呼び出し元の構造体は変わりません。ポインタ渡しでは、構造体のアドレスを渡すため、関数内から呼び出し元の構造体を直接変更できます。

構造体ポインタでアドレスとメンバを確認する

構造体ポインタを使うと、構造体変数のアドレスを確認したり、ポインタ経由でメンバを表示したりできます。

次のプログラムでは、構造体変数のアドレスを関数に渡し、関数内でアドレスとメンバの値を表示します。

プロジェクト/ファイル名: Lesson65_2/main.c

#include <stdio.h>

typedef struct {
    int id;
    double temperature;
} SENSOR;

void showSensor(SENSOR* pSensor) {
    printf("受け取ったアドレス: %p\n", (void*)pSensor);
    printf("id=%d, temperature=%.2f\n", pSensor->id, pSensor->temperature);
}

int main(void) {
    SENSOR sensor = { 10, 26.75 };

    // 構造体変数のアドレスを関数に渡す
    showSensor(&sensor);

    return 0;
}

実行結果例

受け取ったアドレス: 0000008C5A8FF9A8
id=10, temperature=26.75

アドレスの表示結果は、実行環境や実行タイミングによって変わります。
そのため、表示されるアドレスの値が例と同じになるとは限りません。

このプログラムでは、main関数でSENSOR型のsensorを宣言しています。

SENSOR sensor = { 10, 26.75 };

そのアドレスをshowSensor関数に渡しています。

showSensor(&sensor);

showSensor関数では、SENSOR* pSensorとして受け取ります。

void showSensor(SENSOR* pSensor)

pSensorにはsensorのアドレスが入っています。
そのため、pSensor->idやpSensor->temperatureで、sensorのメンバを参照できます。

%pでアドレスを表示するときのポイント

アドレスをprintfで表示するときは、%pを使います。

printf("受け取ったアドレス: %p\n", (void*)pSensor);

%pで表示するときは、一般的にvoid*へキャストして渡します。

書き方意味
pSensor構造体変数sensorのアドレス
(void*)pSensor%pで表示するための形
pSensor->idpSensorが指す構造体のid
pSensor->temperaturepSensorが指す構造体のtemperature

アドレスそのものの値を覚える必要はありません。
大切なのは、pSensorがsensorの場所を指していて、その場所を通してメンバにアクセスしているということです。

(*p).memberとp->memberの関係

構造体ポインタのメンバアクセスは、次の2つの書き方で表せます。

(*pSensor).id

pSensor->id

この2つは同じ意味です。

ただし、(pSensor).idでは、まずpSensorでポインタが指す構造体を取り出し、そのあと.idでメンバにアクセスしています。

pSensor->idは、その処理を簡潔に書ける記法です。

書き方読み方
(*pSensor).idpSensorが指す構造体のid
pSensor->idpSensorが指す構造体のid
(*pSensor).temperaturepSensorが指す構造体のtemperature
pSensor->temperaturepSensorが指す構造体のtemperature

実際のC言語プログラムでは、構造体ポインタのメンバアクセスには->を使うことが多いです。

構造体ポインタと配列の関係

構造体の配列を作ると、同じ型の構造体がメモリ上に連続して並びます。

このとき、配列名は先頭要素のアドレスとして使えます。

DATA list[2];
DATA* p = list;

pはlist[0]を指します。
p + 1はlist[1]を指します。

構造体配列でも、通常の配列と同じようにポインタ演算が使えます。

書き方意味
plist[0]を指す
p->scorelist[0].scoreと同じ意味
p + 1list[1]を指す
(p + 1)->scorelist[1].scoreと同じ意味

構造体配列とポインタを組み合わせると、複数の構造体データを順番に処理できます。

構造体配列をポインタで扱うプログラム

次のプログラムでは、構造体配列を作り、その先頭アドレスをポインタに代入して、各要素のメンバを表示します。

プロジェクト/ファイル名: Lesson65_3/main.c

#include <stdio.h>

typedef struct {
    int score;
    char grade;
} RESULT;

int main(void) {
    RESULT list[3] = {
        { 72, 'B' },
        { 88, 'A' },
        { 65, 'C' }
    };

    RESULT* p = list;

    // 構造体配列の先頭要素を表示する
    printf("1人目の点数: %d 評価: %c\n", p->score, p->grade);

    // ポインタ演算で次の要素を表示する
    printf("2人目の点数: %d 評価: %c\n", (p + 1)->score, (p + 1)->grade);

    // さらに次の要素を表示する
    printf("3人目の点数: %d 評価: %c\n", (p + 2)->score, (p + 2)->grade);

    return 0;
}

実行結果

1人目の点数: 72 評価: B
2人目の点数: 88 評価: A
3人目の点数: 65 評価: C

このプログラムでは、RESULT型の構造体配列listを作っています。

RESULT list[3] = {
    { 72, 'B' },
    { 88, 'A' },
    { 65, 'C' }
};

そして、pにlistを代入しています。

RESULT* p = list;

配列名listは、先頭要素list[0]のアドレスとして使えます。
そのため、pはlist[0]を指します。

p->scoreは、list[0].scoreと同じ意味です。

p->score

p + 1は、次の構造体要素であるlist[1]を指します。
そのため、(p + 1)->scoreでlist[1].scoreにアクセスできます。

(p + 1)->score

構造体配列では、ポインタ演算で次の構造体へ進めることを確認しておきましょう。

図:構造体配列をポインタで順番にたどる

この図から分かること

この図から分かるのは、構造体配列でもポインタ演算を使って要素を順番にたどれるということです。

pはlist[0]を指し、p + 1はlist[1]を指します。各要素は構造体なので、メンバにアクセスするときは->を使います。(p + 1)->scoreのように書くことで、次の構造体要素のscoreメンバを参照できます。

構造体ポインタを使うメリット

構造体ポインタには、いくつかの大きなメリットがあります。

メリット内容
コピーを避けられる構造体全体を渡さず、アドレスだけを渡せる
元のデータを変更できる関数内から呼び出し元の構造体を更新できる
大きな構造体に向いているメンバが多い構造体でも効率よく扱える
配列と相性がよい構造体配列をポインタで順番に処理できる
動的メモリと組み合わせやすいmallocで確保した構造体もポインタで扱える

特に、関数で構造体の内容を変更したい場合は、ポインタ渡しが基本になります。

構造体ポインタを使うときの注意点

構造体ポインタは便利ですが、ポインタである以上、注意点もあります。

NULLポインタを参照しない

構造体ポインタがNULLのまま、p->idのようにアクセスしてはいけません。

BOOK* pBook = NULL;

この状態では、pBookは有効な構造体を指していません。
pBook->idのように使うと危険です。

関数で構造体ポインタを受け取る場合は、必要に応じてNULLチェックを行います。

if (pBook == NULL) {
    return;
}

構造体変数と構造体ポインタを混同しない

構造体変数には.を使います。
構造体ポインタには->を使います。

対象正しいアクセス
BOOK bookbook.code
BOOK* pBookpBook->code

この違いをしっかり区別しましょう。

アドレスを渡すときは&を付ける

構造体変数をポインタ渡しする場合は、&を使ってアドレスを渡します。

updateByPointer(&book2);

book2は構造体変数そのものです。
&book2はbook2のアドレスです。

関数がBOOK*型を受け取るなら、BOOK型の変数ではなく、そのアドレスを渡します。

文字列メンバの扱いに注意する

構造体の中にchar配列のメンバがある場合、文字列の代入にはstrcpyなどを使います。

strcpy(pBook->title, "C言語実践編");

char配列に対して、代入演算子で文字列を直接入れることはできません。

構造体ポインタとconst

構造体ポインタを関数に渡すと、関数内から元の構造体を変更できます。

ただし、表示だけをしたい場合や、関数内で変更してほしくない場合もあります。そのようなときは、constを付ける方法があります。

void printBook(const BOOK* pBook)

このように書くと、pBookが指すBOOKの内容を関数内で変更しないという意図を表せます。

書き方意味
BOOK* pBook指す先の構造体を変更できる
const BOOK* pBook指す先の構造体を変更しない前提で扱う

表示専用の関数では、constを使うと安全性と読みやすさが上がります。

構造体ポインタが役立つ場面

構造体ポインタは、次のような場面でよく使われます。

活用場面内容
関数で構造体を更新するアドレスを渡して元データを変更する
大きな構造体を渡すコピーを避けて効率よく渡す
構造体配列を処理するポインタ演算で要素を順に扱う
動的メモリを使うmallocで確保した構造体をポインタで扱う
データ構造を作るリストや木構造などで構造体同士をつなぐ

構造体ポインタは、C言語で実用的なデータ管理を行ううえで重要な考え方です。

特に、構造体を関数に渡して処理する場面では、値渡しにするか、ポインタ渡しにするかを意識することが大切です。

構造体ポインタを理解するとデータ操作が効率よくなる

構造体ポインタは、構造体変数のアドレスを保存し、そのアドレスを通してメンバを参照したり変更したりするための仕組みです。

構造体変数のメンバにアクセスするときは.を使い、構造体ポインタが指す先のメンバにアクセスするときは->を使います。p->idは、(*p).idを読みやすくした書き方です。

構造体を関数に渡すとき、値渡しではコピーが作られます。そのため、関数内で変更しても呼び出し元には反映されません。ポインタ渡しでは構造体のアドレスを渡すため、関数内から元の構造体を直接操作できます。

Lesson65_1では、値渡しとポインタ渡しの違いを確認しました。
Lesson65_2では、構造体ポインタが受け取るアドレスと、->によるメンバ参照を確認しました。
Lesson65_3では、構造体配列をポインタで順番にたどる方法を確認しました。

構造体ポインタを使えるようになると、構造体を関数で効率よく扱えるようになり、配列や動的メモリ、さらに複雑なデータ構造にもつなげやすくなります。C言語で実用的なプログラムを書くために、構造体ポインタの考え方をしっかり身につけておきましょう。