C LANGUAGE TECHNOLOGY

【C言語】構造体とは【定義,変数,アクセス,引数,返り値,ポインタ,ビットフィールド】

2021年7月1日

悩んでいる人

C言語の構造体を教えて!

こういった悩みにお答えします.

本記事の信頼性

  • リアルタイムシステムの研究歴12年.
  • 東大教員の時に,英語でOS(Linuxカーネル)の授業.
  • 2012年9月~2013年8月にアメリカのノースカロライナ大学チャペルヒル校(UNC)コンピュータサイエンス学部で客員研究員として勤務.C言語でリアルタイムLinuxの研究開発.
  • プログラミング歴15年以上,習得している言語: C/C++PythonSolidity/Vyper,Java,Ruby,Go,Rust,D,HTML/CSS/JS/PHP,MATLAB,Assembler (x64,ARM).
  • 東大教員の時に,C++言語で開発した「LLVMコンパイラの拡張」,C言語で開発した独自のリアルタイムOS「Mcube Kernel」GitHubにオープンソースとして公開
  • 2020年1月~現在はアメリカのノースカロライナ州チャペルヒルにあるGuarantee Happiness LLCのCTOとしてECサイト開発やWeb/SNSマーケティングの業務.2022年6月~現在はアメリカのノースカロライナ州チャペルヒルにあるJapanese Tar Heel, Inc.のCEO兼CTO.
  • 最近は自然言語処理AIイーサリアムに関する有益な情報発信に従事.
    • (AI全般を含む)自然言語処理AIの論文の日本語訳や,AIチャットボット(ChatGPT,Auto-GPT,Gemini(旧Bard)など)の記事を50本以上執筆.アメリカのサンフランシスコ(広義のシリコンバレー)の会社でプロンプトエンジニア・マネージャー・Quality Assurance(QA)の業務委託の経験あり.
    • (スマートコントラクトのプログラミングを含む)イーサリアムや仮想通貨全般の記事を200本以上執筆.イギリスのロンドンの会社で仮想通貨の英語の記事を日本語に翻訳する業務委託の経験あり.

こういった私から学べます.

C言語を独学で習得することは難しいです.

私にC言語の無料相談をしたいあなたは,公式LINE「ChishiroのC言語」の友だち追加をお願い致します.

私のキャパシティもあり,一定数に達したら終了しますので,今すぐ追加しましょう!

友だち追加

独学が難しいあなたは,元東大教員がおすすめするC言語を学べるオンラインプログラミングスクール5社で自分に合うスクールを見つけましょう.後悔はさせません!

構造体とは

構造体とは,データをグループ化して取り扱うための機能です.

例えば,ディスプレイ上の点はx座標とy座標の2次元座標からなります.

2次元座標を扱う時は,x座標とy座標を別々にして扱うより,グループにしてデータを管理した方が良いですよね.

実際にコードを書くことを考えてみましょう.

まず,利用する変数を配列で定義します.

扱える座標の上限を100個とすると,以下のようになります.

次に,x座標の小さい順に座標をソートするsort_point_by_x関数を以下のように書く場合を考えます.

座標はx座標とy座標の1グループで意味があるので,xとyは1グループとして扱われるべきです.

しかし,上記の例では,x座標とy座標を各々独立した配列として扱っています.

この場合,x座標を元にソートする時に,同時にy座標の配列も一緒にソートしなければなりません.

このように別々の配列として定義するとソートし忘れてバグが発生する可能性が高くなります.

また,sort_point_by_x関数では1グループとして扱うデータの個数がx座標とy座標の2個ですが,AIの分野等でよく利用されるn次元の座標の場合はデータの個数が増えてくると引数が多くなり,コードが複雑になってしまいます.

このような課題を解決してくれるのが構造体です.

sort_point_by_x関数では,x座標とy座標を1グループのパッケージ(構造体)と考え,その構造体の配列を考えれば良いことになります.

構造体の定義

それでは,実際に2次元座標の構造体を定義してみましょう.

struct point構造体の定義は以下になります.

この構造体に付けられている名前はpointであり,正確には「構造体タグ」(タグ名)と呼びます.

また,構造体の中の各要素(変数)であるint型のx,yは「メンバ」と呼びます.

構造体の定義の書式は,以下のようになります.

構造体タグと変数名は省略可能です.

変数名を省略すると,構造体の定義のみが行われます.

構造体の変数

次に,構造体の変数を定義しましょう.

構造体の最後の右カッコ”}”の後には,変数のリストを書くことができます.

以下のように記述することで,struct point型の変数a,b,cを定義します.

考え方としてはa,b,cという名前が付いた箱があり,その箱にはint型の値を入れることができる2つの引き出しが付いています.

その引き出しには,さらにxとyという名前が付いています.

この書式は,構文的には以下と同じです.

struct point型を定義した後に以下のように記述すると,この構造体の変数a,b,cを定義できます.

例えば,struct point型を外部で定義すると,スコープ内の(その下の)各関数では,以下のように変数を定義できます.

もし,その構造体をローカルにしか利用しないのであれば,構造体タグを省略できます.

この場合,構造体に名前を付けないので,構造体変数の定義は以下のように1回しか(構造体の定義の直後しか)行うことができません.

typedefを利用して構造体変数をうまく定義する方法がよく利用されます.

例えば,前の例だと,以下のように定義が長くなってしまいます.

これが嫌な時には,typedefを利用して新しい型を作成します.

typedefは,文法的には記憶クラス指定子に分類されますが,新しい方を作成できます.

typedefの書式は,以下のようになります.

typedefを利用すると以下のように記述できます.

このように書くと,struct point型を新たにPOINT型として利用できます.

つまり,以下の2つは両方とも同じ変数の定義になります.

typedefを利用する場合,構造体タグを省略することが多いです.

また,構造体タグは,typedefを利用しない場合でも省略可能です.

例えば,以下のように構造体タグpointは省略できます.

typedefを学びたいあなたはこちらからどうぞ.

構造体のアクセス

構造体のアクセスについて説明します.

よく利用される書き方ですので,きちんと理解しましょう.

構造体メンバへのアクセス

構造体の各々のメンバにアクセスするためには,以下のように記述します.

ここで,ピリオド”.”は「構造体メンバ演算子」と呼ばれ,構造体名とメンバとも結びつける演算子です.

例えば,struct point型の変数aが,以下のように定義されているとします.

a.xとa.yに点(100,200)を代入したい場合,以下のように書きます.

このように,「構造体名.メンバ」として普通の変数と同じように扱うことができます.

構造体変数の初期化

構造体変数も,普通の変数と同じように初期化することができます.

構造体変数の定義の後に,各メンバに対応する定数式の並びを付け加えます.

以下のように記述すると,a.xが100でa.yが200を初期値とするstruct point型の変数aを定義できます.

配列の初期化と同様です.

構造体変数の入れ子

構造体は,入れ子(ネスト)にして利用できます.

例えば,円は,その中心座標と半径で表すことができます.

円を表す構造体の構造体タグをcircleとすると,以下のように書くことができます.

このstruct circle型の変数oは,これまでと同様に,以下のように定義できます.

中心座標が(10,20)で半径が5の円を表すには,以下のように記述します.

それでは,定義と同時に初期化してみましょう.

初期化方法は,配列の初期化の時と同様です.

次は,三角形を考えてみましょう.

三角形は,3点の座標で表されるので,構造体タグをtriangleとすると以下のように書けます.

struct circle型の場合と同様に,struct triangle型の各々のメンバにアクセスできます.

また,同様に初期化もできます.

構造体を関数の引数に指定

構造体を関数の引数に指定する方法を紹介します.

構造体も,普通の変数のように関数に渡すことができます.

これまでの普通の変数の時と同様に,関数の引数に構造体を書けば良いです.

例えば,以下のようにしてget_distance関数を作成できます.

普通の引数と同様に,仮引数p1とp2にはローカルなコピーが渡されます.

したがって,p1とp2の値を変更しても,呼び出し元の実引数の値は変更しません.

get_distance関数を利用するコードは以下になります.

このコードは,2点の平面座標を読み込んで,その2点間の距離を計算します.

最初にstruct point型を定義しています.

次に,struct point型を引数としたdouble型のget_distance関数のプロトタイプ宣言をしています.

このように構造体が引数の場合でも,引数が普通の変数のプロトタイプ宣言と同じになります.

main関数では,struct point型の配列pを定義しています.

そして,この配列pに2点の座標をそれぞれ読み込み,これらをget_distance関数に渡しています.

このときの配列の利用方法は,これまでの普通の変数の時と同様です.

get_distance関数は,struct point型の引数を2つ受け取り,その2点間の距離をdouble型で返す関数です.

最後に,読み込んだ座標と,その2点間の距離を表示します.

get_distance関数では,2点間の距離を計算しています.

このアルゴリズムはピタゴラスの定理(三平方の定理)ですので,どのようにプログラムに変換されているのかを理解しましょう(下式).

$$z^2 = x^2 + y^2$$

また,原点から点\((x, y)\)までの距離zをユークリッド距離と呼びます(下式).

$$z = \sqrt{x^2 + y^2}$$

ここで,sqrt関数はdouble型の引数を取り,その平方根を返り値として返す関数です.

このコードをコンパイルする時の注意点ですが,sqrt関数を利用するためには6行目にあるmath.hファイルをインクルードする必要があります.

math.hでプロトタイプ宣言されているライブラリ関数を利用する際,gccコンパイラではリンクオプションの-lmを入れる必要があります.

平方根を計算するsqrt関数やユークリッド距離を計算するhypot関数を知りたいあなたはこちらからどうぞ.

実行結果は以下になります.

3~6行目で座標(1,1)と(2,3)を入力したら,get_distance関数は距離を2.236068(\(\simeq \sqrt{5}\))と返します.

また,9~12行目で座標(100,100)と(200,300)を入力したら,get_distance関数は距離を223.606798(\(\simeq 100 \sqrt{5}\))と返します.

構造体を関数の返り値に設定

普通の変数と同様に,構造体を関数の返り値に設定することができます.

構造体を関数の返り値に設定するコードは以下になります.

int型の変数を2つ読み込み,それらをstruct point型の変数aに代入し,それぞれをx座標とy座標とします.

その際,struct point型を返すget_point関数を利用しています.

そして,代入されていることを表示して確認しています.

実行結果は以下になります.

3行目の行末でx座標の1,4行目の行末でy座標の2を入力したら,get_point関数でstruct point型のメンバ変数xとyにそれぞれ代入された構造体の変数を返り値として取得できていることがわかります.

構造体のポインタ渡し

構造体も,普通の変数と同様にポインタを利用できます.

特に,構造体の場合は,ポインタがよく利用されます.

この理由として,メモリ効率等がポインタ渡しの方が優れているからです.

構造体のメモリサイズが大きい場合,構造体を関数に渡すたびにローカルなコピーをすると,メモリ効率が悪くなったり,プログラムの実行速度が低下したりする可能性があります.

したがって,メモリサイズが大きい構造体を扱うときにはポインタ渡しの方が好まれます.

それでは,実際に構造体のポインタを定義しましょう.

構造体のポインタも普通の変数のポインタと同様に定義できます.

以下のように記述すると,変数aはstruct point型の変数であり,変数paは構造体struct point型へのポインタ変数になります.

paがaを指すには,以下のように書きます.

これも,普通の変数の時と同じですね.

paは構造体の内容を意味するので,(*pa).xと(*pa).yは,それぞれa.xとayと同じ意味になります.

このとき,カッコ()は必ず入れましょう.

この理由として,構造体メンバ演算子”.”はポインタ演算子”*”よりも優先順位が高いからです(それぞれの優先順位は1位と2位).

もしカッコを付けないと*pa.xは*(pa.x)の意味なので,バグが発生してしまいます.

演算子の優先順位の詳細を知りたいあなたは,演算子の優先順位と結合規則を読みましょう.

ポインタpaを利用してaの値を点(10,20)とするには以下のようにします.

このようにして,構造体の各々のメンバにアクセスできます.

しかし,構造体のポインタを利用して各々のメンバにアクセスするためには,いつもカッコ()を書かなくてはダメというわけではありません.

カッコを書かなくてもよいように,専用の演算子「構造体メンバ演算子”->”」が用意されています.

構造体メンバ演算子を利用することで,(*pa).xをpa->xと書くことができます.

構造体メンバ演算子で上記の例を書き直すと以下のようになります.

get_distance関数をポインタで書き直したコードは以下になります.

具体的には,get_distance関数のプロトタイプ宣言と実装が変更していますが,同じ処理結果になることを確認して下さい.

実行結果は以下になります.同じ結果です.

C言語のコードを書きなれていない場合は,無理に構造体をポインタ渡しにする必要はありません.

また,ポインタ自体にあまり慣れていない場合はやめたほうが無難です.

現在のコンピュータは,メモリや処理速度等は,あまり気にする必要はありません.

したがって,構造体を利用する場合,ポインタを利用しないと明らかに無理がある場合(例:数M~数Gバイトのサイズの大きい構造体)以外は,値渡しの方が安全できれいなコードが書けます.

まずは,読みやすくて保守しやすいコードを作成することを心がけましょう.

ただし,C言語の中級者~上級者の方は,是非ポインタ渡しを習得しましょう.

ポインタ演算子を知りたいあなたは,ポインタとはを読みましょう.

構造体のビットフィールド

構造体にはビットフィールドで変数を保持することができます.

ビットフィールドとは,1のメモリ領域(バイト単位)に名前をつけてビット単位に振り分けるものです.

struct point構造体のメンバ変数x,yの下位4ビットをビットフィールドと定義する場合は,以下のようになります.

このように記述することで,下位4ビットのみ有効なメンバ変数を定義できます.

ただし,ビットフィールドのメンバ変数のアドレスは取得できないことに注意して下さい.

get_distance_by_pointer.cでビットフィールドを下位4ビットのみ有効にしたget_distance_by_pointer_with_bit_field.cは以下になります.

24,27行目のscanf関数の引数でビットフィールドのメンバ変数のアドレスは渡せません.

なので,int型変数のxとyのアドレスを渡して入力した値を取得した後に,構造体のメンバ変数p[i].xとp[i].yにそれぞれ代入します(25,28行目).

実行結果は以下になります.

3~6行目で座標(1,1)と(2,3)を入力した場合は同じ結果です.

これに対して,9~12行目で座標(100,100)と(200,300)を入力した場合,座標が(4,4)と(-8,-4)として計算されています.

この理由は,下表の10進数,16進数の関係を読み解くとわかります.

10進数16進数16進数(下位4ビット)10進数(下位4ビット)
1000x640x44
2000xc80x8-8※
3000x12c0xc-4※

※ビットフィールドの最上位の4番目のビットを符号ビットとして計算

Linuxカーネルでは,構造体のビットフィールドはinclude/linux/cdrom.hのstruct cdrom_device_info構造体で利用されています.

メンバ変数のoptions,mc_flags,sanyo_slot,keeplocked,reservedで指定したビット数分のビットフィールドを保持します.

Linuxカーネルを学びたいあなたは,こちらからどうぞ.

まとめ

C言語の構造体(定義,変数,アクセス,引数,返り値,ポインタ,ビットフィールド)を紹介しました.

構造体のように変数をグループとして管理する機能は,C言語だけでなく多くのプログラミング言語で利用されるので,是非習得しましょう.

構造体とよく似たデータ構造として共用体や列挙型があります.

共用体や列挙型を学びたいあなたは,共用体unionの使い方と実例列挙型enumの使い方と実例を読むことをおすすめします.

C言語を独学で習得することは難しいです.

私にC言語の無料相談をしたいあなたは,公式LINE「ChishiroのC言語」の友だち追加をお願い致します.

私のキャパシティもあり,一定数に達したら終了しますので,今すぐ追加しましょう!

友だち追加

独学が難しいあなたは,元東大教員がおすすめするC言語を学べるオンラインプログラミングスクール5社で自分に合うスクールを見つけましょう.後悔はさせません!

-C LANGUAGE, TECHNOLOGY
-, , , , , , , , , , ,