構造体の構造

導入

C 言語の組み込みデータ型のうち、最も基本的なプリミティブ型を除けば、配列だけが複合型であり、同時に複数の値を含めることができますが、同じ型のデータのみを含めることができます。実際の使用では十分です。

実際の使用では、主に次の 2 つの状況があり、より柔軟で強力な複合型が必要になります。

  • 複雑なオブジェクトは複数の変数を使用して記述する必要があります。これらの変数はすべて関連しているため、それらを接続する何らかのメカニズムを用意することが最善です。
  • 関数によっては複数のパラメータを渡す必要があるため、それらを 1 つずつ順番に渡すのは非常に面倒です。それらを複合構造に結合して渡すのが最善です。

これらの問題を解決するために、C 言語は struct キーワードを提供します。これにより、カスタム複合データ型が異なる型の値を結合できるようになります。これにより、プログラミングが便利になるだけでなく、コードの可読性も向上します。 C 言語には、他の言語のようなオブジェクトやクラスの概念がありません。構造体は、オブジェクトやクラスの機能の大部分を提供します。

以下は「struct」カスタム データ型の例です。

構造体の分数 {
  int 分子;
  int 分母;
};

上の例では、分数データ型 structfraction を定義しており、これには 2 つの属性 numeratordenominator が含まれています。

カスタム データ型として、その型名には struct キーワードが含まれている必要があることに注意してください。たとえば、上の例は structfraction ですが、スクリプトで別の fraction という名前を定義することもできます。ただし、これは混乱を引き起こしやすいです。また、struct 文の最後のセミコロンは省略できないとエラーが発生しやすくなります。

新しいデータ型を定義した後、他の型の変数を宣言するのと同じ方法で、その型の変数を宣言できます。

構造体部分 f1;

f1.numerator = 22;
f1.分母 = 7;

上記の例では、structfraction 型の変数 f1 が最初に宣言されており、この時点でコンパイラは f1 にメモリを割り当て、その後 f1 のさまざまな属性に値を割り当てることができます。ご覧のとおり、構造体の属性はドット (.) で表されます。たとえば、numerator 属性は f1.numerator と記述する必要があります。

カスタム型の変数を宣言するときは、型名の前に struct キーワードを追加することを忘れないようにしてください。言い換えれば、変数は structfraction f1 を使用して宣言する必要があり、fraction f1 として記述することはできません。

属性に値を 1 つずつ割り当てるだけでなく、中括弧を使用して構造体のすべての属性に一度に値を割り当てることもできます。

構造車 {
  char* 名前;
  変動価格;
  内部速度;
};

struct car saturn = {"サターン SL/2", 16000.99, 175};

上記の例では、変数 saturn の型は struct car であり、その 3 つのプロパティには同時に中括弧内の値が割り当てられます。中括弧内の値の数が属性の数より少ない場合、不足している属性は自動的に「0」に初期化されます。

中括弧内の値の順序は、構造体の型が宣言されているときの属性の順序と一致している必要があることに注意してください。それ以外の場合は、値ごとに属性名を指定する必要があります。

struct car saturn = {.speed=172, .name="サターン SL/2"};

上記の例では、宣言されたプロパティよりも少ないプロパティが初期化され、残りのプロパティは「0」に初期化されます。

変数を宣言した後、属性の値を変更できます。

struct car saturn = {.speed=172, .name="サターン SL/2"};
サターン.スピード = 168;

上記の例では、「speed」属性の値を「168」に変更します。

構造体のデータ型宣言文と変数宣言文を一つの文にまとめることができます。

構造体ブック {
  文字タイトル[500];
  char 著者[100];
  浮動小数点値;
} b1;

上記のステートメントは、データ型 book とこの型の変数 b1 の両方を宣言しています。型識別子 book がこの 1 か所でのみ使用され、後で使用されない場合は、ここで型名を省略できます。

構造体 {
  文字タイトル[500];
  char 著者[100];
  浮動小数点値;
} b1;

上記の例では、struct は匿名データ型を宣言し、次にこの型の変数 b1 を宣言します。

他の変数宣言ステートメントと同様に、宣言中に変数に値を割り当てることができます。

構造体 {
  文字タイトル[500];
  char 著者[100];
  浮動小数点値;
} b1 = {「ハリー・ポッター」、「J.K. ローリング」、10.0},
  b2 = {「がん病棟」、「アレクサンドル・ソルジェニーツィン」、7.85};

上記の例では、変数 b1b2 を宣言しながら、それらに値を代入しています。

次の章で紹介する typedef コマンドでは、構造体のエイリアスを指定できるため、使用が簡単になります。

typedef struct cell_phone {
  int セル番号;
  float minutes_of_charge;
} 電話;

電話機 p = {5551234, 5};

上記の例では、phonestruct cell_phone のエイリアスです。

ポインタ変数は「struct」構造体を指すこともできます。

構造体ブック {
  文字タイトル[500];
  char 著者[100];
  浮動小数点値;
}* b1;

// または 2 つのステートメントとして記述します
構造体ブック {
  文字タイトル[500];
  char 著者[100];
  浮動小数点値;
};
構造体ブック* b1;

上記の例では、変数 b1 はポインターであり、ポイントされるデータは struct book 型のインスタンスです。

構造体は配列メンバーとしても機能します。

構造体小数番号[1000];

数値[0].numerator = 22;
数値[0].分母 = 7;

上の例では、1000 個のメンバーを持つ配列 numbers を宣言しており、各メンバーはカスタム型 fraction のインスタンスです。

構造体が占有する記憶領域は、各属性の記憶領域の合計ではなく、メモリ フットプリントが最も大きい属性の記憶領域の倍数になります。他の属性はそれに合わせてギャップを追加します。これにより、読み取りと書き込みの効率が向上します。

構造体 foo {
  int a;
  char* b;
  文字c;
};
printf("%d\n", sizeof(struct foo)); // 24

上記の例では、struct foo には 3 つの属性があります。64 ビット コンピュータ上で占有される記憶域スペースは、次のとおりです。 int a は 4 バイトを占有し、ポインタ char* b は 8 バイトを占有し、char c は占有します。 1バイト。これらを合計すると、合計 13 バイト (4 + 8 + 1) になります。しかし、実際には、struct foo は 24 バイトを占有します。その最大のメモリ占有属性は char* b の 8 バイトであり、その結果、他の属性の記憶領域も 8 バイトになるため、アライメントの結果は次のようになります。 struct foo 全体は 24 バイト (8 * 3) です。

余分なストレージスペースは空きスペースで埋められているため、上記の struct foo の実際の構造は次のとおりです。

構造体 foo {
  int a; // 4
  char Pad1[4]; // 4 バイトを埋め込みます。
  char *b;
  文字 c; // 1
  char Pad2[7]; // 7 バイトを埋め込みます。
};
printf("%d\n", sizeof(struct foo)); // 24

なぜメモリの調整にこれほど多くのスペースを浪費するのでしょうか?これは、メモリ フットプリントを同じ長さのブロックに分割することで、Struct 構造内の各属性の開始アドレスをすばやく見つけることができるようにするため、読み取りと書き込みを高速化します。

この機能により、必要に応じて、Struct 構造を定義するときに、記憶領域の昇順に各属性を定義できるため、領域を節約できます。

構造体 foo {
  文字c;
  int a;
  char* b;
};
printf("%d\n", sizeof(struct foo)); // 16

上記の例では、最小のスペースを占める char c が最初にランク付けされ、次に int a が続き、最大のスペースを占める char* b が最後にランク付けされます。 `strct foo' 全体のメモリ フットプリントは 24 バイトから 16 バイトに削減されます。

構造体のコピー

構造体変数は、代入演算子 (=) を使用して別の変数にコピーできます。この場合、まったく新しいコピーが生成されます。システムは元の変数と同じサイズの新しいメモリ空間を割り当て、そこに各属性をコピーします。つまり、データがそのままコピーされます。これは配列のコピーとは異なるので注意してください。

struct cat { char name[30] } a, b;

strcpy(a.name, "フラ");
a.年齢 = 3;

b = a;
b.name[0] = 'M';

printf("%s\n", a.name); // フラ
printf("%s\n", b.name); // ムラ

上の例では、変数 b は変数 a のコピーです。 2 つの変数の値は独立しています。 b.name を変更しても a.name には影響しません。

上記の例は、データをコピーするには構造体の属性が文字配列として定義されている必要があるという前提に基づいています。これを少し変更して属性を文字ポインターとして定義すると、結果は異なります。

struct cat { char* 名前 } a, b;

a.name = "フラ";
a.年齢 = 3;

b = a;

上の例では、name 属性が文字ポインタになり、このとき、ab に割り当てられるため、b.name は同じアドレスを指す同じ文字ポインタになります。 2 つの属性が同じアドレスを共有していること。このとき、構造体には前の例の配列ではなくポインタが格納されているため、コピーされるのは文字列そのものではなく、そのポインタです。また、文字ポインタが指す文字列は変更できないため、この時点では文字列を変更できません。

要約すると、代入演算子 (=) は、構造体の各属性の値を別の構造体変数に正確にコピーできます。これは配列とはまったく異なります。代入演算子を使用して配列をコピーしても、データはコピーされず、アドレスが共有されるだけです。

この割り当てでは 2 つの変数が同じ型である必要があり、異なる型の構造体変数を相互に割り当てることはできないことに注意してください。

さらに、C 言語には 2 つのカスタム データ構造が等しいかどうかを比較する方法が用意されておらず、比較演算子 (==!= など) を使用して 2 つのデータ構造が等しいか等しくないかを比較することはできません。

構造体ポインタ

構造体変数を関数に渡すと、関数内で取得されるのは元の値のコピーです。

#include <stdio.h>

構造体タートル {
  char* 名前;
  イワナ*種;
  年齢;
};

void happy(struct Turtle t) {
  t.age = t.age + 1;
}

int main() {
  struct Turtle myTurtle = {"MyTurtle", "ウミガメ", 99};
  幸せ(私のカメ);
  printf("年齢は %i\n", myTurtle.age); // 出力 99
  0を返します。
}

上記の例では、関数 happy() が構造体変数 myTurtle に渡され、関数内にインクリメント操作があります。しかし、happy() を実行した後、関数の外側の age 属性の値はまったく変化していません。その理由は、関数内で取得されるのは構造体変数のコピーであり、コピーを変更しても関数外の元のデータには影響を与えないためです。

通常、開発者は、同じデータが関数に渡されると、関数内でデータが変更された後、そのデータが関数の外部に反映されることを望みます。さらに、同じデータが渡されるため、プログラムのパフォーマンスの向上にも役立ちます。このとき、関数に構造体変数のポインタを渡し、そのポインタを介して構造体の属性を変更する必要があるため、関数の外部に影響を与える可能性があります。

関数に構造体ポインタを渡す書き方は以下の通りです。

void happy(struct Turtle* t) {
}

幸せ(&myTurtle);

上記のコードでは、「t」は構造体のポインタであり、関数を呼び出すときにポインタが渡されます。構造体の型は配列とは異なります。型識別子自体はポインタではないため、渡すときはポインタを &myTurtle として記述する必要があります。

ポインタから構造体自体を取得するには、関数内で書き込みメソッド (*t).age も使用する必要があります。

void happy(struct Turtle* t) {
  (*t).年齢 = (*t).年齢 + 1;
}

上の例では、ドット演算子 . の優先順位が * よりも高いため、(*t).age*t.age として記述することはできません。 *t.age を記述する方法では、t.age をポインタとして扱い、それに対応する値を取得するため、予測できない結果が生じます。

ここで、上記の例全体を再コンパイルして実行すると、happy() 内の構造体に対する操作が関数の外側に反映されます。

「(*t).age」のように書くのは非常に面倒です。 C 言語には、構造体ポインターから属性を直接取得できる新しい矢印演算子 (->) が導入されており、コードの可読性が大幅に向上しています。

void happy(struct Turtle* t) {
  t->年齢 = t->年齢 + 1;
}

要約すると、構造体変数名の場合はドット演算子 (.) を使用して属性を取得し、構造体変数ポインターの場合は矢印演算子 (->) を使用して属性を取得します。変数 myStruct を例にとると、ptr をそのポインタとすると、以下の 3 つの書き方は同じです。

// ptr == &myStruct
myStruct.prop == (*ptr).prop == ptr->prop

構造体のネスト

struct 構造体のメンバーは、別の struct 構造体になることができます。

構造体の種類 {
  char* 名前;
  int 型;
};

構造体魚 {
  char* 名前;
  年齢;
  構造種の繁殖。
};

上記の例では、「fish」の属性「breed」は別の構造体「species」です。

値を代入するときに値を記述する方法は数多くあります。

// 書き方その1
struct 魚のサメ = {"サメ", 9, {"セラチモルファ", 500}};

//書き方2
構造体 myBreed = {"Selachimorpha", 500};
struct 魚のサメ = {"サメ", 9, myBreed};

//書き方3
構造体フィッシュシャーク = {
  .name="サメ",
  .age=9、
  .breed={"セラキモルファ", 500}
};

// 書き方4
構造体フィッシュシャーク = {
  .name="サメ",
  .age=9、
  .breed.name="セラチモルファ",
  .breed.kinds=500
};

printf("サメの種は %s", shark.breed.name);

上の例は、ネストされた Struct 構造に対する 4 つの割り当て方法を示しています。さらに、breed プロパティの内部プロパティを参照するには、ドット演算子 (shark.breed.name) を 2 回使用します。

次に、ネストされた構造体の別の例を示します。

構造体名 {
  文字が最初[50];
  最後の文字[50];
};

構造体学生 {
  構造体名 名前;
  不足;
  チャーセックス。
学生1;

strcpy(student1.name.first, "ハリー");
strcpy(student1.name.last, "ポッター");

// または
構造体名 myname = {"ハリー", "ポッター"};
Student1.name = 私の名前;

上の例では、カスタム タイプ studentname 属性が別のカスタム タイプです。後者のプロパティを参照したい場合は、student1.name.first などの 2 つの . 演算子を使用する必要があります。 。また、文字配列属性に値を割り当てるには、 strcpy() 関数を使用する必要があります。文字配列名のアドレスを直接変更するとエラーが発生するため、値を直接割り当てることはできません。

struct 構造体は、他の構造体を内部的に参照できるだけでなく、自己参照もできます。つまり、現在の構造体がその構造体によって内部的に参照されます。たとえば、リンクリスト構造のノードは次のように記述できます。

構造体ノード {
  int データ;
  構造体ノード* 次;
};

上の例では、「node」構造の「next」属性は、別の「node」インスタンスへのポインタです。次に、この構造を使用してデータ リンク リストをカスタマイズします。

構造体ノード {
  int データ;
  構造体ノード* 次;
};

構造体ノード* ヘッド;

// 3 つのノードのリストを生成します (11)->(22)->(33)
head = malloc(sizeof(struct ノード));

ヘッド->データ = 11;
head->next = malloc(sizeof(struct ノード));

ヘッド->次->データ = 22;
head->next->next = malloc(sizeof(struct ノード));

ヘッド->次->次->データ = 33;
head->next->next->next = NULL;

// このリストを反復処理します
for (struct ノード *cur = head; cur != NULL; cur = cur->next) {
  printf("%d\n", cur->data);
}

上記の例は、リンク リスト構造の最も単純な実装であり、「for」ループを介して実行できます。

ビットフィールド

struct は、「ビット フィールド」と呼ばれるバイナリ ビットで構成されるデータ構造を定義するために使用することもできます。これは、基礎となるバイナリ データを操作するのに非常に役立ちます。

構造体 {
  unsigned int ab:1;
  unsigned int cd:1;
  unsigned int ef:1;
  unsigned int gh:1;
シンセサイザー;

シンセ.ab = 0;
シンセ.cd = 1;

上記の例では、各属性の後の「:1」は、これらの属性が 1 つのバイナリ ビットのみを占めることを示しているため、このデータ構造には合計 4 つのバイナリ ビットがあります。

バイナリ ビットを定義する場合、構造内の各属性は整数型のみであることに注意してください。

実際に格納する際、C 言語は int 型が占めるバイト数に応じてビットフィールド構造を格納します。残りのビットがある場合は、名前のないプロパティを使用してそれらのビットを埋めることができます。幅 0 の属性を使用することもできます。これは、現在のバイトの残りのバイナリ ビットが埋められ、次の属性が次のバイトに強制的に格納されることを意味します。

構造体 {
  unsigned int フィールド 1 : 1;
  符号なし整数: 2;
  unsigned int フィールド 2 : 1;
  符号なし整数: 0;
  unsigned int フィールド 3 : 1;
} もの;

上の例では、stuff.field1stuff.field2 の間に、2 バイナリ ビットの幅を持つ名前のないプロパティがあります。 stuff.field3 は次のバイトに格納されます。

柔軟な配列メンバー

多くの場合、配列に含まれるメンバーの数を事前に決定することはできません。多数のメンバーを含む配列を事前に宣言すると、スペースが無駄になります。 C 言語は、フレキシブル配列メンバーと呼ばれるソリューションを提供します。

配列メンバーの数を事前に決定できない場合は、構造体を定義できます。

構造体vstring {
  int len;
  char chars[];
};

上の例では、struct vstring 構造には 2 つの属性があります。 len 属性は、配列 chars の長さを記録するために使用されます。 chars 属性は配列ですが、メンバーの数は指定されません。

chars 配列に含まれるメンバーの数は、vstring にメモリを割り当てるときに決定できます。

struct vstring* str = malloc(sizeof(struct vstring) + n * sizeof(char));
str->len = n;

上記の例では、「chars」配列のメンバーの数が「n」であると仮定していますが、「n」がどれだけあるかは実行時にのみ知ることができます。次に、「struct vstring」に必要なメモリを割り当てます。これは、「struct vstring」が占有するメモリ長に、「n」個の配列メンバーが占有するメモリ長を加えたものです。最後に、「len」属性は「n」が何であるかを記録します。

これにより、事前に決定する必要がなく、配列 charsn 個のメンバーを持つことができ、実行時のニーズと一致させることができます。

Elastic Array メンバーには特別なルールがいくつかあります。まず、弾性メンバーの配列が構造体の最後の属性である必要があります。さらに、フレックス配列メンバーに加えて、構造体には少なくとも 1 つの他の属性が必要です。


作者: wangdoc

アドレス: https://wangdoc.com/

ライセンス: クリエイティブ・コモンズ 3.0