支援対象地域:札幌、仙台、関東、愛知、関西、広島、福岡

  • TOP
  •   
  • コラム
  •   
  • C言語で学ぶ「構造体」と「ポインタ」

1.はじめに

たとえば、学校で各学年のデータベースを作成する際、その生徒を示す「学生番号や名前、年齢」といった概念に対して複数の変数を割り当ててひとまとめに取り扱うことを「構造体」と言います。「構造体」を使用することは、これらをひとまとめに宣言してそれをもとに各個別ごとで入れ物を作成して項目ごとにデータを入れていくことができます。つまり、個別にそれぞれの項目を用意する手間が省け、型が違うものでも1つの塊として扱えるということです。
当記事では「構造体」の使い方と「ポインタ」の使い方について展開します。

2.構造体

1.構造体とは

「構造体」とは、ある対象に関連する項目を「1つのかたまり」としてまとめたものを言います。
 関連する項目は「メンバ」と呼ばれ、変数や文字列などをメンバーにすることが可能です。ただし、関数をメンバーにすることは不可能です。 構造体は「実体」と呼ばれるオブジェクトを生成して使用していきます。「実体」から「メンバ」を呼び出すことで、値を代入して使用していきます。また、同じように項目をまとめて1つのかたまりとしてものに「配列」というものがありますが、こちらの場合は同じ型のモノを1つのかたまりにすることしかできません。一方で「構造体」は「配列」とは異なってint型や文字列など型が違うものでも1つのかたまりとしてまとめあげることができます。

2.構造体の使い方

以下は構造体の宣言の方法です。

    構造体を定義する構文

  • 1.strukt 構造体名 {
  • 2.データ製1(メンバ名1)
  • 3.データ製2(メンバ名2)
  • 4.
  • 5.};

構造体の定義は、ヘッダファイルに記述するのが一般的です。構造体を定義したヘッダファイルをインクルードすることで、さまざまなプログラムから構造体をデータ型として利用することが可能です。
構造体を作成する際、1行目の「strukt」句というキーワードを使用して宣言します。2行目以降に構造体の中身を形成するメンバ変数を入れていきます。イメージとしては、「構造体」はタンスや本棚、クローゼットなどといった収納庫の役割を果たし、「メンバ」はバケツや箱といったモノ(データ)を詰める容器の役割を果たしています。
上段の定義に加えて「typedef」句を使った宣言の方法もあります。

  • 1.typedef strukt 構造体名 {
  • 2.データ製1(メンバ名1)
  • 3.データ製2(メンバ名2)
  • 4.
  • 5.}; 構造体名2;

上段のような、「struct 構造体名」というデータ型を「構造体名2」というデータ型に型を付け替える方法になります。以下は上段2つの型を合わせたサンプルです。

    サンプルコード1

  • 1.strukt person1 {
  • 2.cher *name;
  • 3.cher sex;
  • 4.int age;
  • 5.cher *add;
  • 6.cher *job;
  • 7.};
  • 8.
  • 9.//構造体
  • 10.typedef strukt person {
  • 11.cher *name
  • 12.cher sex;
  • 13.int age
  • 14.cher *add
  • 15.cher *job
  • 16.}; person2;
  • 17.

上段のコードは2つの宣言方法を活用しています。しかし、「typedef」句を使用した宣言の場合、省略が可能です。

    サンプルコード1

  • 1.typedef strukt{
  • 2.cher *name;
  • 3.cher sex;
  • 4.int age;
  • 5.cher *add;
  • 6.cher *job;
  • 7.}; person2

3.初期化する方法

構造体の実体を定義して初期化する方法は、前述のやり方とは異なり、構造体の実体を生成する記述が違ってきます。

    「struct 構造体名」型の場合、以下ように定義します。

  • 1.struct 構造体名 実体名;

    「typedef」句を使った「構造体名」型の場合、以下ように定義します。

  • 1.構造体名 実体名;

構造体の初期化は、全メンバへの一括代入は実体の宣言時のみ可能であり、それ以外を一括代入することは不可能です。また構造体のメンバは、実体からドット演算子を使用して呼び出します。
いかはそのサンプルコードになります。

  • 1.#include <stdio.h>
  • 2.
  • 3.//構造体
  • 4.strukt person1
  • 5.cher *name;
  • 6.cher sex;
  • 7.int age;
  • 8.cher *add;
  • 9.cher *job;
  • 10.};
  • 11.
  • 12.//構造体
  • 13.typedef strukt person {
  • 14.cher *name
  • 15.cher sex;
  • 16.int age
  • 17.cher *add
  • 18.cher *job
  • 19.}; person2;
  • 20.
  • 21.// 一括代入用の関数
  • 22.person2 init(char *name, char sex, int age, char *add, char* job) {
  • 23.person2 p2;
  • 24.p2.name = name;
  • 25.p2.sex = sex;
  • 26.p2.age = age;
  • 27.p2.add = add;
  • 28.p2.job = job;
  • 29.return p2;
  • 30.}
  • 31.
  • 32.int main(void) {
  • 33.//構造体の実体を生成
  • 34.struct person1 nakagawa;
  • 35.tanaka.name = "R.Nakagawa";
  • 36.tanaka.sex = 'm';
  • 37.tanaka.age = 36;
  • 38.tanaka.add = "Tokyo";
  • 39.tanaka.job = "teacher";
  • 40.printf("%sは%d歳で、%sで%sをしています\n", nakagawa.name,nakagawa.age, nakagawa.add, nakagawa.job);
  • 41.
  • 42.//構造体の実体の生成と一括初期化
  • 43.person2 sato = {"S.Saitou", 'f', 25, "Osaka", "nurse"};
  • 44. /* error: expected expression before '{' token
  • 45.saitou = {"S.Saitou", 'f', 25, "Osaka", "nurse"};
  • 46. */
  • 47.
  • 48. printf("%sは%d歳で、%sで%sをしています\n", saitou.name, saitou.age, saitou.add, saitou.job);
  • 49.
  • 50.//関数で一括代入
  • 51. saitou = init("M.Saitou", 'm', 35, "Nagoya", "doctor");
  • 52.printf("%sは%d歳で、%sで%sをしています\n", saitou.name,saitou.age, saitou.add, saitou.job);
  • 53. return 0;
  • 54.}

    実行結果

  • 1.R.Nakagawaは36歳で、Tokyoでteacherをしています
  • 2.S.Saitouは25歳で、Osakaでnurseをしています
  • 3.M.Saitouは35歳で、Nagoyaでdoctorをしています

上段の構造体では「struct person1」型と「person2」型で宣言して、「struct person1」型を実体化する際には「struct person1 tanaka」と定義しています。
実体「nakagawa」のメンバの初期化は各メンバを1つずつ初期化していき、「person2」型を実体化する際には「person2 sato」と定義します。
実体「saitou」のメンバの初期化は定義と一緒に全メンバ一括で行っています。定義とは別に実体を一括代入しようとしてもコンパイルエラーとなりますので注意がいります。実体の各メンバの代入を1つずつ行っていくよりも一括代入用の関数「init」を記述し使用します。

3.ポインタ

1.ポインタとは

「ポインタ」とは、変数のアドレスを記憶する変数をいいます。アドレスとは、メモリ上に与えられた「番号」を指し、変数を宣言するとその変数にアドレス、つまり、「メモリ上の番号」が与えられます。
アドレスにアクセスすることで「変数の値」に取得することが可能です。また、アドレスはデフォルトとして16進数で表されており、ポインタ変数は整数の加減算が行えるだけでなく、メモリ上の番号の演算やアドレス先の値を取得することも可能です。

    ポインタを使用するメリット

  • ・アドレス先の値を取得したり、変更することが可能。特に関数の引数でポインタを使用することで「参照渡し」を行うことで、複数の変数を1つの関数で処理変更することが可能。
  • ・1つのポインタ変数で、配列のすべての要素の値を取得、変更することが可能。
  • ・1つのポインタ変数で、構造体のすべてのメンバの値を取得・変更が可能であるため、メンバを1つずつ取り扱う手間が省ける。
  • ・1つのポインタ変数で、複数の関数の中から使用する関数を選択できるため、後からでも処理内容を変更することが可能。

2.ポインタの使い方

「ポインタ変数」は、変数名の前に「*(アスタリスク)」を付けて「今からポインタを使う」ということを宣言します。ポインタ変数には「変数のアドレス」を代入します。
変数のアドレスは変数名の前に「&(アンパサンド)」を付けて表します。つまり、変数とポインタ変数は同じ型ある必要があります。以下はサンプルコードです。

  • 1.#include <stdio.h>
  • 2.
  • 3.int main(void) {
  • 4.int num = 1; //int型変数
  • 5.int *p_num; //int型ポインタ変数
  • 6.p_num = # //ポインタ変数p_numに変数numアドレスを代入
  • 7.
  • 8.printf("int型変数numの値:%d\n", num);
  • 9.printf("int型ポインタ変数p_num:%p\n", p_num);
  • 10.
  • 11.return 0;
  • 12.}

    実行結果

  • 1.int型変数numの値:1
  • 2.int型ポインタ変数p_num:0x7ffd0a0ecc6

上段において「int型」のポインタ変数「p_num」を宣言します。5行目のポインタ変数「p_num」に、6行目にある同じ「int型」の変数「num」のアドレス「&num」を代入します。ポインタ変数を8,9行目の「printf関数」で出力表示する際には、アドレスを表示するために「%p」変換子を使用します。

3.アドレスの使い方

変数のアドレスは、変数名の前に「&(アンパサンド)」を付けて表示します。変数のアドレスが変数のアドレスがポインタ変数の値となります。
変数のアドレスの前に「*(アスタリスク)」を付けることでアドレス先の値の取得が可能です。
以下はサンプルです。

  • 1.#include <stdio.h>
  • 2.
  • 3.int main(void) {
  • 4.int num = 1; //int型変数
  • 5.int *p_num; //int型ポインタ変数
  • 6.p_num = # //ポインタ変数p_numに変数numアドレスを代入
  • 7.
  • 8.printf("int型変数numのアドレス:%p\n", &num);
  • 9.printf("int型変数numのアドレス先の値:%d\n", *(&num));
  • 10.printf("int型ポインタ変数p_numの参照先の値:%d\n", *p_num);
  • 11.
  • 12.return 0;
  • 13.}

    実行結果

  • 1.int型変数numのアドレス:0x7ffd0a0ecc64
  • 2.int型変数numのアドレス先の値:1
  • 3.int型ポインタ変数p_numの参照先の値:1

上段のサンプルにおいて、int型変数「num」のアドレス「&num」をprintof関数を使用して出力表示します。加えて、アドレス「&num」の前に「*(アスタリスク)」を付けてアドレス先の値を取得表示します。
また、アドレス「&num」を代入したポインタ変数「p_num」の前に「*(アスタリスク)」を付けて、同様にアドレス先の値を取得し表示します。

4.ポインタ、アロー演算子の使い方

構造体の実態は、ポインタを使用すると、全メンバを含む実体全体を扱うことが可能です。
実体のポインタからメンバを呼び出す際には「->(ハイフンと大なり)」を使用します。以下は構文とサンプルです。

    構文

  • 1.構造体の実体のポインタ変数名->メンバ名

    サンプル

  • 1.#include <stdio.h>
  • 2.
  • 3.//構造体
  • 4.strukt person1
  • 5.cher *name;
  • 6.cher sex;
  • 7.int age;
  • 8.cher *add;
  • 9.cher *job;
  • 10.}; person2;
  • 11.
  • 12.int main(void) {
  • 13.person2 kudo, *p1;
  • 14.p1 = &kudo; // 実体のアドレス
  • 15.p1->age = 17; // ポインタを使ってメンバの初期化
  • 16.printf("%d\n", kudo.age);
  • 17.
  • 18.person2 sugimoto, *p2;
  • 19.p2 = &sugimoto; // 実体のアドレス
  • 20.*p2 = *p1; // アドレス先の値を共有
  • 21.printf("%d\n", kato.age);
  • 22.
  • 23.return 0;
  • 24.}

    実行結果

  • 1.17
  • 2.17

上段での構造体の実体「kudo」のアドレス先のポインタ「p1」を使用します。
ポインタ「p1」のメンバ「age」のアドレスにアロー演算子「->」を使ってアドレス先の値にアクセスして初期化を行います。また、実体「sugimoto」のアドレス先の値に実体「kudo」のアドレス先の値を代入します。これにより、実体「sugimoto」のメンバ「age」の値も実体「kudo」のメンバ「age」と同じ値となります。

4.まとめ

「構造体」は、同じ型のモノしかひとまとめにすることができない配列と異なり、型のとなるモノでもひとまとめに扱えます。加えて、メンバ全体をまとめて扱う場合、ポインタを使用することで実体のアドレス先の値を操作できます。それにより、配列や関数、構造体等の複雑な構成を簡単な記述で扱えます。