構造体とポインタ | Programming Place Plus 新C++編

トップページ新C++編

このページの概要

このページでは、構造体に絡むポインタ関連の話題をまとめて取り上げます。構造体を指し示しているポインタを使ってデータメンバにアクセスする際に使えるアロー演算子や、考え方が特殊なメンバポインタという機能、ポインタ型のデータメンバが自分が所属する構造体を指す自己参照構造体。そして、どこも指し示していないヌルポインタという考え方についても取り上げます。

以下は目次です。要点だけをさっと確認したい方は、「まとめ」をご覧ください。



アロー演算子

ここまでのページで、配列とポインタの関係については説明してきましたが、構造体とポインタに関しての説明をあまりしていませんでした。このページでは、バイナリエディタのプログラムからはいったん離れて、構造体とポインタに関する残りの話題を取り上げておきます。構造体とクラスは実質的に同じものなので(「構造体」のページを参照)、このページの内容はクラスでも同様です。


たいていの構造体は大きいので、関数に普通に渡すと処理時間が長くなる恐れがあります。これを避けるためには、参照型を使うことが考えられます。

// 参照型で受け取る。元の値を書き換えないなら const も併用
void print_book_title(const Book& book)
{
    std::cout << book.title << "\n";
}

もう1つの方法として、ポインタを使う手があります。

#include <iostream>
#include <string>

struct Book {
    std::string    title;
    std::string    author;
    int            price;
    int            evaluate;
};

void print_book_title(const Book* book)
{
    std::cout << (*book).title << "\n";
}

int main()
{
    Book book {"C++ Programming Guide Book", "Thomas Murray", 3500, 5};
    print_book_title(&book);
}

実行結果:

C++ Programming Guide Book

構造体を指し示すポインタを経由して、データメンバにアクセスします。そのためには、まず間接参照を行い Book型の値を得てから、データメンバにアクセスするという流れになるので、(*book).title という少し面倒な記述になっています。

この面倒な記述は、-> という2文字の記号によるアロー演算子(矢印演算子) (arrow operator) を使って改善できます。

std::cout << book->title << "\n";

構造体を指し示すポインタ p に対する p->m は、(*(p)).m と同じ意味になります1

ポインタを真似して作られているイテレータの場合も同様で、構造体を指し示すイテレータを経由して、データメンバにアクセスするときにもアロー演算子が使えます。

auto it = std::cbegin(books);
std::cout << it->title << "\n";

効率の面からいえば、参照型を使う方法とポインタを使う方法に差はありません。どちらにしても、普通に構造体をコピーで渡すよりは効率的になります。しかし、ポインタの場合には、そのポインタが適切なオブジェクトを指し示せているかどうかを考慮する必要があります。これが次の話題です。

ヌルポインタ

ポインタには「何も指し示していない」という状態が存在します。このようなポインタを、ヌルポインタ(ナルポインタ、空ポインタ) (null pointer) と呼びます。参照型には「何も指し示していない」という状態はありえないので、これはポインタと参照の重大な違いといえます。

ヌルポインタは何も指し示していないので、間接参照をしたり、++ や – などでアドレス計算をしたりすることは未定義動作です。そのため、ポインタ型の変数や仮引数を扱うときには、ヌルポインタになっている可能性を考慮しなければなりません。この厄介さを避けるために、ポインタよりも参照型を使うというのは良い考えですが、ポインタを完全に避けることが難しい現実もあります。


変数を定義するとき、初期化子を空の {} にした場合は値初期化されますが(「構造体」のページを参照)、ポインタ型の値初期化はヌルポインタによる初期化です。

int* p1 {};     // ヌルポインタ
int* p2 {&n};   // ヌルポインタではない

int* p3;        // 自動ストレージ期間を持つ場合は未初期化
                // 静的ストレージ期間を持つ場合はヌルポインタ

未初期化の場合、当然ヌルポインタであることも期待できません。

静的ストレージ期間を持ち、初期化子がない場合、プログラム開始時に行われるゼロ初期化のみが適用されます(「メモリとオブジェクト」のページを参照)。ゼロ初期化は 0 で初期化することですが、後述するルールによって、0 はヌルポインタとして扱われます。

明示的にヌルポインタであることを示すために、nullptr キーワードを使用できます。

int* p4 {nullptr};

nullptr はヌルポインタを表すリテラルで、その型は std::nullptr_t です。一口にポインタといっても、int*char**const int* のように、さまざまな型が存在しますが、std::nullptr_t は、どの型のポインタにでも暗黙的に変換できます

【上級】関数ポインタやメンバポインタ(後述)にも変換できます。

なお、あとで取り上げますが、ヌルポインタを表現する方法が nullptr 以外にも存在しています。いずれかの方法を使って表現されるヌルポインタをまとめて、ヌルポインタ定数 (null pointer constant) と呼びます2


ポインタ型は bool型に暗黙的に変換でき、ヌルポインタなら false に、そうでなければ true になります。そのため以下のような記述が可能です。

if (p) {   // p != nullptr と同じ
}

if (!p) {  // p == nullptr と同じ
}

このように簡潔に書くほうがいいか、ポインタであることが明確に分かるように書いたほうがいいかは意見が割れるところですが、C++ Core Guidelines では省略することを推奨しています3。新C++編もこれに従うことにします。

【C++20】ポインタ型から bool型への変換を、縮小変換とみなすことになりました4

なお、nullptr 同士の比較は true になります。

想定外のヌルポインタによるバグは非常に多く、過去に何度も重大な問題を起こしている現実があります。次のプログラムのように、assert を使ってチェックしたり、if文で回避したりします。

#include <cassert>
#include <iostream>
#include <string>

struct Book {
    std::string    title;
    std::string    author;
    int            price;
    int            evaluate;
};

void print_book_title(const Book* book)
{
    assert(book);
    std::cout << book->title << "\n";
}

int main()
{
    Book book {"C++ Programming Guide Book", "Thomas Murray", 3500, 5};
    print_book_title(&book);

    Book* book_ptr {&book};
    print_book_title(book_ptr);

    Book* book_ptr2 {};
    print_book_title(book_ptr2);
}

実行結果:

C++ Programming Guide Book
C++ Programming Guide Book
Assertion failed: book, file c:\main.cpp, line 14

0 や NULL による表現

ヌルポインタ定数を表現するには、nullptr 以外にも、整数リテラルの 0 を使う方法や、NULLマクロを使う方法があります。

int* p1 {0};
int* p2 {NULL};

nullptr は C++11 で追加されたものであり、それより前は 0 や NULL を使っていました。これらの方法はトラブルを招くケースが多少あるので、C++11 以降では迷わず nullptr を使いましょう。なお、ヌル文字 ('\0') をヌルポインタと混同して使っているコードをまれに見かけますが、これはあくまで文字なので、ヌルポインタとは異なるものです。

0 でヌルポインタを表現できるのは、ヌルポインタ定数とは、整数リテラル 0 または nullptr であると定義されているためです2。勘違いしやすいところですが、「ヌルポインタのメモリ上での表現も 0 が並んでいる」と考えるのは間違いです。ヌルポインタがメモリ上でどのように表現されるかは処理系によって異なります。

【上級】std::memset関数などを使ってメモリ上を 0 で埋めても、それをポインタ型の値としてみたとき、ヌルポインタとみなされない可能性があるということです。

【C++98/03 経験者】nullptr がない C++98/03 では、ヌルポインタ定数とは整数リテラルの 0 のことでした5

【C言語プログラマー】C言語のヌルポインタ定数は、整数定数の 0 あるいは、それを void* にキャストしたものとされています6。C++ では後者の可能性は削除されています。

一方、NULL は標準ライブラリの <cstddef> などに定義されているマクロで、ヌルポインタ定数を表現する実装定義の定数に置換されます7。それはつまり整数リテラルの 0nullptr です。

【C++98/03 経験者】C++98/03 でも NULL に関する定義は同じですが8、C++98/03 のヌルポインタ定数は必ず整数リテラルの 0 なので、置換結果は 0 です。型は処理系によって違うかもしれません(0L など)。

【C言語プログラマー】C言語でも NULL に関する定義は同じですが9、C言語のヌルポインタ定数は整数定数の 0 か、void* にキャストされた 0 なので、置換結果はこれらのいずれかになります。

ヌルポインタ定数として 0 を使うことを避けたほうがいいのは、0 自体はあくまでも整数型だからです。プログラマーの意図と、コンパイラの型の判断が食い違うおそれがあります。たとえば、auto p = 0; としたとき、プログラマーはヌルポインタのつもりだったかもしれませんが、当然 int に型推論されます。

【上級】関数オーバーロードの問題もあります。関数f を、仮引数が int のものと、char* のものにオーバーロードしているとき、f(0) はどちらを呼び出すでしょうか? 0 はあくまでも整数型なので、ヌルポインタのつもりだったとしても、int型バージョンの関数を呼びます。

NULL を避けたほうがいいのは、これがマクロだからです。マクロは極力使わないほうがいいです(「プリプロセス」のページを参照)。また、置換結果が処理系によって nullptr になったり、0 になったりするので、ソースコードの移植性を落とすことになります。

【上級】たとえば、さきほどのコラムの例で、f(NULL) はどちらの関数を呼び出すでしょう? NULL を 0 に置換する処理系では int型バージョンを、nullptr に置換する処理系では char*型バージョンを呼ぶことになります。

自己参照構造体

構造体のデータメンバの型を、その構造体型のポインタ型にできます。以下の Person構造体には、Person*型のデータメンバが含まれています。

struct Person {
    std::string  name;         // 名前
    Person*      next_person;  // 次の人へのポインタ
};

このような構造体を、自己参照構造体 (self referencing struct) と呼びます。

これは、Person構造体の中にまた Person構造体があるのとは違うことに注意してください(それだと無限に繰り返される構造になってしまうので不可能です)。next_person はポインタなので、Person構造体そのものではなく、メモリ上のどこかにある Person構造体オブジェクトを指します(あるいはヌルポインタ)。なお、参照型で代用することはできません。

コンパイルが通るプログラムを見たほうが分かりやすいかもしれません。

#include <iostream>
#include <string>

struct Person {
    std::string  name;
    Person*      next_person;
};

int main()
{
    Person person1 {"Tanaka Yuuta", nullptr};
    Person person2 {"Sugimoto Hikaru", nullptr};
    Person person3 {"Ando Yukie", nullptr};

    // 次に来る人を next_person に指定する
    person1.next_person = &person2;
    person2.next_person = &person3;

    // 先頭の人から順番に名前を出力
    Person* p {&person1};
    while (p) {
        std::cout << p->name << "\n";
        p = p->next_person;
    }
}

実行結果:

Tanaka Yuuta
Sugimoto Hikaru
Ando Yukie

3人の人をあらわす変数を定義して、next_person に「次の人」のメモリアドレスを設定しています。最後の人であることは、next_person が nullptr であることで判断できます。next_person の中身を辿っていけば、先頭の人から最後の人まで順番にアクセスできます。next_person に格納する値を変えれば、好きなように並び順を変更できることが分かると思います。

この例のように、複数のデータをポインタなどの手段を使って接続(リンク)し、任意につなげ変えられるデータ構造を、連結リスト (linked list) と呼びます。

連結リストは自力で実装できますが、通常は標準ライブラリにある連結リスト(std::forward_list、std::list)を使うのがいいです。

連結リストについては、アルゴリズムとデータ構造編でも解説しています。

メンバポインタ

構造体変数ではなく、そのデータメンバを指し示すポインタを作ることもできます。

#include <iostream>

struct S {
    int a;
    double b;
};

int main()
{
    S s {10, 2.5};
    int* p {&s.a};  // s のデータメンバ a を指し示すポインタ

    std::cout << *p << "\n";
    *p = 20;
    std::cout << *p << "\n";
}

実行結果:

10
20

これはデータメンバを1つの変数としてみれば、納得のいくコードだと思います。

これとは別の考え方で、データメンバにアクセスするポインタを実現する方法があって、メンバポインタ (member pointer) や メンバへのポインタ (pointer to member) と呼ばれています(今後はメンバポインタで統一します)。

【上級】メンバ関数を指し示すポインタ(メンバ関数ポインタ)も実現でき、これも含めてメンバポインタと呼びます。メンバ関数ポインタは、「クラス」のページで取り上げます。

メンバポインタはその名のとおりポインタの一種のようでありながら、実際にはかなり異なる存在です。メンバポインタは、構造体を指し示しているポインタ(あるいは構造体のオブジェクト)とともに使うことで、そのデータメンバにアクセスします。データメンバを直接指し示しておらず、データメンバのメモリアドレスを知っているわけでもありません。イメージとしては、構造体内でのデータメンバの位置(先頭から何バイト目にあるかというオフセット的な情報)を持っていると考えられます。

さきほどのコードをメンバポインタを使って書き替えると、次のようになります。

#include <iostream>

struct S {
    int a;
    double b;
};

int main()
{
    S s {10, 2.5};
    S* ps {&s};           // 構造体変数 s を指し示すポインタ
    int S::* pm {&S::a};  // 構造体 S のデータメンバ a を指し示すメンバポインタ

    // 構造体変数s を指し示すポインタを経由して、データメンバ a をアクセスする。
    ps->*pm = 20;
    std::cout << ps->*pm << "\n";

    // 構造体変数s を経由して、データメンバ a をアクセスする。
    s.*pm = 30;
    std::cout << s.*pm << "\n";
}

実行結果:

20

メンバポインタ変数を宣言する構文は以下のとおりです。

データメンバの型 構造体型::* 識別子 初期化子;
データメンバの型 構造体型::* 識別子;

「初期化子」がないときのルールはいつものとおり、自動ストレージ期間を持つなら未初期化、静的ストレージ期間を持つならゼロ初期化です(「メモリとオブジェクト」のページを参照)。ゼロ初期化の結果はヌルポインタになります。「初期化子」を {} とした場合、値初期化によってヌルポインタになります。

メンバポインタの値は「ある構造体変数のデータメンバのアドレス」ではなく、「ある構造体型のデータメンバの位置情報」でなければなりません。そのため「初期化子」には、&s.a のようなデータメンバのアドレスを記述することはできず、&S::a のように記述します。これは以下の構文によるものです。

&構造体型::データメンバ名

メンバポインタを使ってデータメンバを間接参照するには、->*.* という演算子を用いた、次の構文を使います。

構造体を指し示すポインタ->*メンバポインタ
構造体型のオブジェクト.*メンバポインタ

「構造体を指し示すポインタ」や「構造体型のオブジェクト」の構造体は、メンバポインタを宣言したときに使った構造体型でなければなりません。また、「構造体を指し示すポインタ」が nullptr の場合は未定義動作です。

メンバポインタが持っているのは、構造体型内でのデータメンバの位置なので、「構造体を指し示すポインタ」や「構造体型のオブジェクト」の部分を取り換えることで、別の構造体の同じデータメンバにアクセスできます。

#include <iostream>

struct S {
    int a;
    double b;
};

int main()
{
    S s1 {10, 2.5};
    S s2 {20, -2.5};
    double S::* pm {&S::b};  // データメンバ b を指すメンバポインタ

    // 1つのメンバポインタを使って、s1、s2 のデータメンバ b にアクセスする
    std::cout << s1.*pm << "\n";
    std::cout << s2.*pm << "\n";

    // s1 のデータメンバを書き換える
    S* p {&s1};
    p->*pm *= 2.0;
    std::cout << s1.*pm << "\n";
    std::cout << s2.*pm << "\n";  // s2 のデータメンバは変更されていない
}

実行結果:

2.5
-2.5
5
-2.5

実際のところ、メンバポインタが役に立つ場面はあまりありません。どのデータメンバにアクセスするべきか分かっているのなら、最初に示した普通のポインタによる方法を使えばいいです。アクセスするデータメンバを切り替えたいような場合には、メンバポインタをうまく使えるかもしれません。

constメンバポインタ

普通のポインタに constポインタがあるように(「配列とポインタ」のページを参照)、constメンバポインタ (const member pointer) もあります。考え方は同じで、データメンバが const である場合は、constメンバポインタでなければ指し示すことができません。また、constメンバポインタを経由してデータメンバの値を書き換えることはできません。

constメンバポインタは次のように宣言します。「データメンバの型」に const が含まれているなら、それだけでも構いません。

const データメンバの型 構造体型::* 識別子;
データメンバの型 const 構造体型::* 識別子;

以下はプログラム例です。

#include <iostream>

struct S {
    int a;
    double b;
    const int c;
};

int main()
{
    S s {10, 2.5, 999};

    const int S::* pa {&S::a};
    s.*pa = 100;  // エラー。constメンバポインタ経由では書き換えられない
    std::cout << s.*pa << "\n";

    double const S::* pb {&S::b};
    s.*pb = 1.5;  // エラー。constメンバポインタ経由では書き換えられない
    std::cout << s.*pb << "\n";

    const int S::* pc {&S::c};
    s.*pc = 0;  // エラー。constデータメンバは書き換えられない
    std::cout << s.*pc << "\n";

    int S::* p {&S::c};  // エラー。データメンバが const なので、constメンバポインタでなければならない
    std::cout << s.*p << "\n";
}

一方、次の宣言は constメンバポインタではなく、メンバポインタ変数が const修飾されていることになります。この場合、書き換えられないのは、メンバポインタ変数自身です。

データメンバの型 構造体型::* const 識別子;

以下はプログラム例です。

#include <iostream>

struct S {
    int a;
    double b;
    int c;
};

int main()
{
    S s {10, 2.5, 999};

    int S::* const pa {&S::a};
    s.*pa = 100;  // OK
    std::cout << s.*pa << "\n";

    pa = &S::c;   // エラー。pa は const修飾されているので、書き換えられない
    s.*pa = 100;
    std::cout << s.*pa << "\n";
}

constメンバポインタでありながら、メンバポインタ変数自身も const修飾された状態にすることも可能です。

const データメンバの型 構造体型::* const 識別子;
データメンバの型 const 構造体型::* const 識別子;

まとめ


新C++編の【本編】の各ページには、末尾に練習問題があります。ページ内で学んだ知識を確認する簡単な問題から、これまでに学んだ知識を組み合わせなければならない問題、あるいは更なる自力での調査や模索が必要になるような高難易度な問題をいくつか掲載しています。


参考リンク


練習問題

問題の難易度について。

★は、すべての方が取り組める入門レベルの問題です。
★★は、自力でプログラミングができるようなるために、入門者の方であっても取り組んでほしい問題です。
★★★は、本格的にプログラマーを目指す人のための問題です。

問題1 (確認★)

以下の変数のうち、ヌルポインタとして初期化されるものをすべて選んでください。

int* p1 {nullptr};
int* p2 {0};
int* p3 {NULL};
int* p4 {};
int* p5 {p1};
int* p6 {0L};
int* p7;

解答・解説

問題2 (基本★)

以下のように定義された構造体型の変数2つの値が一致しているかどうかを判定する関数を作成してください。引数はポインタで渡すものとします。

struct Book {
    std::string    title;
    std::string    author;
    int            price;
    int            evaluate;
};

解答・解説

問題3 (応用★★)

タイトルあるいは著者名を使って本を検索する関数を、次のように宣言しました。中身を作成してください。

Book* search_book(std::vector<Book>& books, std::string Book::* item, const std::string& name)

解答・解説


解答・解説ページの先頭



更新履歴




はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る