見出し画像

Retro-gaming and so on

GLib入門 GArray編

何度も書いてるが、実はC言語には高級言語で言うトコの配列もない
直にメモリを触る言語、ってのがCの特徴で、ハッキリ言うが、C言語の実態はマジで高級言語と言うより毛が生えた程度のアセンブリだ。

毛が生えた程度のアセンブリ

この「メモリを直に触る」と言うのをメリットと捉える人もいるし、デメリットと捉える人もいるだろう。
いや、ハッキリ言えば「かつてはメリットだった」けど、2023年現在だと「デメリット」だと考えた方が良い。「かつては」と言うのは、これも何度も言うが、民生機のPCのスペックが貧弱だったから、だ。
16bit機でメモリが多くて2Mbyteくらいしかない(2Gじゃないぞ?)・・・とかそんなスペックだったら、マジで「本物の高級言語」なんぞ動かなかったんだ。
従って「偽物の高級言語」であるC言語で「メモリを直に弄る」のが効率的だったんだ・・・いや、当時だとC言語でさえ「非効率と嫌われていた」。コンパイラが吐き出すアセンブリ言語や機械語なんぞ「熟練プログラマ」は信用してなかった。速度が必要なら彼らは直にアセンブリ言語や機械語でコーディングする方を好んだんだ。
しかし、2023年現在、富豪の時代、だとCPUはかつてのミニコンより速い。メモリも考えられないくらいたくさんある。そんな中で、「本物の配列」を持たないC言語で「直にメモリを弄る」なんつーのは面倒くさいだけ、だ。
そこで、ってワケでもねぇが(笑)、GLIBはC言語に「本物の高級言語の配列」を提供する。それが今回扱うGArrayだ。

GArrayも凄い。特徴が2つある。

  1. 可変長配列である。
  2. 「長さ」の情報を持っている。
平たく言うと、GArrayはC言語の「動的配列」と呼ばれるモノをラッピングしている。従って、フツーの(C言語の)「配列」とは違って、使用後にはメモリを解放しなきゃいけない。
ただし、メリットとしては、素のC言語の「動的配列」の伸長を自動でやってくれる。これは大きい。プログラマ側がメモリのリアロケート(再確保)を考えなくていい、って事だ。GArrayは2の累乗でメモリを再確保してくれる(つまり、1 -> 2 -> 4 -> 16 -> ...とだんだん大きくメモリを再確保する、って事だ)。
しかし、もっと大きなメリットは何と言っても「GArrayは配列の長さの情報を持ってる」トコだろう。
前に書いたが、配列だろうと動的配列だろうと、C言語は宣言時にn個の要素を持った「型に従った」メモリ領域を用意してくれるが、生憎「終端情報」を持たない。そのため、簡単に「用意されたメモリの範囲外」にデータを書き込めてしまうんで、極悪なバグを簡単に作り込んでしまえるんだ。
バッファオーバーフローとか、あるいはセグメンテーションフォルトとかはこれで起きる。「境界問題」として知られるエラーだ。C言語を使った事がある人は毎回これのお世話になる(笑)。「どう気をつけようと」必ず間違えるわけだ。
しかしGArrayは「長さの情報」がある。と言う事はC言語のクセに生意気にも(笑)比較的安全なプログラミングが行える、と言う事だ。
よって、GLibを入手したとすれば、もう「素のC言語の配列/動的配列」とはオサラバしてGArrayを愛用しよう。GStringと共に「GLib利用のプログラミング」では基礎データ型となるだろう。

GArrayの定義は以下のようなモンだ。


おっそろしい程簡単な定義だ。構造体でgchar*型のdata(つまり配列)とguint型のlenの2つだけを持ってる。これだけ、でC言語の境界問題を回避出来るのなら、如何に初代のC言語が手抜き実装、っつーか手抜き設計したのか分かろうと言うもんだ(笑)。
ちなみに、この定義は以前見たGStringの定義とほぼ同じだ。GStringよりメンバ/スロット/フィールドの数が1つ少ないだけ、だ。
つまり、機能的に見ると、GArrayとGStringはほぼ同じだ、って事なんだけど、取るべきデータの想定が違う、って事なんだな。
ちょっとその辺説明しよう。
まずgchar*型だけど、これは素のC言語のchar*型へのエイリアスだ。そして以前書いた通り、char*は単純には素のC言語での文字列として考えて良い。これがGStringのstrになるわけだ。
一方、GArrayの想定は違う。同じchar*型なんで機能的には同じだが、こっちはメモリ上のバイトの塊へのポインタのつもりなんだ。
C言語の仕様上、char自体は「文字」を特に表さない。何度も書いてるが、1バイトが何ビットかの正確な定義はないが、何にせよ、charは1バイトの大きさ、として定義されてて、short、int、long等と比べると最小のビット幅なんだ。
言い換えるとバイト単位で扱える最小量をcharと言う「大きさ」が保証してる、って事になる。
そしてここがC言語ユーザーの悪癖なんだ。まず、繰り返すが、「byte」と言うのはシステム依存でハッキリしたモノではない。従って、8bit幅を想定した「塊」を扱いたいのなら、少なくともC99(JIS C)以降ではint8_t型を使うべき、なんだ(※1)。GLibならgint8を使う局面だ。その方が意味が明解になる筈なんだが、C言語ユーザーは「俺は過去からこう書いてきたんだからこれでいい」ってのが多すぎるんだ。
まぁ、GLibはANSI C/C89/C90でも使えるって話なんでポータビリティの為にこうしてんのかもしんないけど、真似しないように。バイト単位で扱いたい、かつ8bit幅想定なら、charを使わずにint8_tやGLibならgint8を使うようにしよう
いずれにせよ、GArrayは「バイトの塊」へのポインタをデータとしている。
さぁ、GArrayの単純な使い方を見てみよう。



GArrayの宣言はg_array_newを用いて行う。



g_string_newは3引数関数で、第一引数、第二引数共に真偽値(gboolean)を受け取る。第三引数で「想定するデータ型のサイズ」を指定する。この辺は素のC言語の動的配列そのまんまだ。
問題は第一引数と第二引数なんだけど、第一引数はGArrayの終端に0を置くかどうか訊いてて(TRUEを指定するとそうなる)、第二引数はGArrayの要素を削除した際に、それを0に置き換えるかどうか尋ねてる(TRUEを指定するとそうなる)。
結局この辺は、「GArrayも文字列として使える」ようにそうしてるらしいが、GStringがあるから要らんだろ(笑)。結果、第一引数も第二引数も、通常はFALSEを与えておけばいい筈だ。
GArrayに値を追加する際にはg_array_append_valと言うマクロを使う。



特徴は次の2つだ。

  1. 可変長配列であるGArrayに対して、ケツに値を追加していく。
  2. 与える値は変数名じゃないとならない。
1番は、この辺でGSListとの差がまずある。GSListの場合、ケツに値を追加する際、NULLポインタとの連結を一旦切って繋ぎ直さなければならない辺りコストがかかった。結果、GSListの場合、「前方から」GSList単位を追加する方が簡単だ。
一方、GArrayは真逆で、先頭に値を追加する方がコストがかかる。と言うのも、GArray内の「要素全体をコピーして」先頭を空けて次の位置からペーストする、と言う「配列要素の複製の手間」ってのがかかる。一方、ケツに要素を追加するのはそのまま挿入すればいいので速いんだ。
GSListは先頭追加が有利、GArrayは後方追加が有利、と覚えておこう。
2はそのまんま、だ。値は常に別に変数として作ったモノを代入しないとならない。これがg_array_append_valの制限、となる。
次。要素を参照するにはg_array_indexマクロを使う。



第一引数に対象とするGArray、第二引数に返り値の型指定、第三引数に要素番号を指定する。
このマクロはなかなか賢く、第二引数に返り値の型指定が出来るのは大きい。これにより、GLibにありがちな「キャスト」(型変換)がここでは不要になってるんだ。
なお、GSListで「目的の要素を引き摺り出す」際には、必ずGSListの先頭から要素を探していかないとならない。つまり、シーケンシャルアクセスがリストの特徴で、それが「柔軟な」片方向連結リストの欠点だ。
一方、GArrayは配列なので、要素番号を指定するだけでその値にすぐアクセス出来る。ランダムアクセスが配列の有利な点だ。
GArrayから要素を削除するにはg_array_remove_indexを使う。



これは返り値はあるが、破壊的変更目的の関数だ。第二引数で指定された要素番号の場所の値を第一引数で指定されたGArrayから削除し、「詰める」。ここでもGArrayの要素のコピーとペーストが「自動で」行われる、と言う事だ。上のプログラムの例だと、GStringの"there"が削除された後、二番目にあったGStringの"world"が一番目の位置へと「移動する」事になる。
そしてGArrayのメモリを解放するにはg_array_freeを使う。



g_array_freeの第一引数には対象となるGArray、第二引数には真偽値(gboolean)を与える。マニュアルによると、GArrayの「ガワ」だけfreeしたい場合にはFALSEを与え、ラップしてる動的配列そのものもfreeしたい場合はTRUEを与えろ、って書いてんだけど良く分からん(笑)。
まぁ、その辺はほっといて、いかりや長介的に「次、行ってみよう」(謎



なお、GArrayは構造体なんで、長さを知りたい場合はアロー演算子でlenを引きずり出せば長さが簡単に分かる。



g_array_sized_newは殆どg_array_newと同じなんだけど、要素数をプログラマ側が指定する事が出来る。ただし、先にも書いた通り、GArrayは可変長配列で、そのサイズは2の累乗毎に増えていくんで、自分でサイズを記述する際にも2の累乗毎にしておいた方が良い。
いずれにせよ、これは最初にある程度のメモリサイズを与えておいて、メモリのリアロケート(再確保)を避けるように初期化する為に使う。
何度も書くが、GArrayは可変長配列だ。しかし、場合によってはメモリの再確保が頻繁に起きてパフォーマンスが低下する事がありうる。そういう可能性がある場合、人為的に「デカくメモリを再確保」しといた方がいい。その時に使うのがg_array_set_sizeだ。繰り返すが、可変長配列であるGArrayは2の累乗毎にメモリを再確保するように設計されてるんで、それに逆らわず、人為的にメモリを再確保する際にも2の累乗になるように確保した方がいい。



次は一番あり得るGArrayの使い方、だ。つまり、「素のC言語の性的静的な配列」をGArrayでラップする方法だ。



名前は似てるがg_array_append_valは単一の値を追加するマクロで、g_array_append_valsは複数の値を追加する関数だ。



第一引数は値を後からはめ込みたいGArray、第二引数はgpointer型のデータ、第三引数にはめ込みたいデータの「長さ」を記述する。
第二引数の型がgpointerだ、と言う事はvoid*型なんで、どんなデータでもはめ込める。上のコード例だと性的静的配列であるx、yを直接指定してるが、ここでも「C言語はメモリの塊を操れない」事を思い出そう。配列名はある領域の先頭のアドレスを意味してるので「そのまま代入が可能」となる。
あとは、g_array_append_valsが面倒を見てくれて、長さ情報に従って、性的静的配列の各要素へのポインタを格納してくれる。
g_array_prepend_valsは機能的にはg_array_append_valsと同じだが、先頭から値を追加する。先にも書いたが、動的配列の構造上、「後に値を追加する」方が簡単で、先頭に追加する場合は既存の動的配列の中身をコピーして全部後へとペーストしなきゃならない。従って演算コストがかかる。



次は「データの挿入」だ。



g_array_insert_valは単一の値を挿入する為のマクロ、g_array_insert_valsは複数の値を挿入する為の関数で、機能的にはこの2つは似通っている。
繰り返すが、「挿入」も既存のGArrayが持ってる要素の位置をコピペでズラさなければならないので、演算コストはかかる。
また、複数の値を挿入する際、もし全部の値が分かってるのなら、g_array_insert_valを何度も使うより、g_array_insert_valsで一気に挿入した方がコストはかからない。



次は「データの削除」だ。



GLibではGArrayの要素削除に3つの方法を提供してる。

  1. g_array_remove_index(GArray* array, guint index_): arrayからindex_番目の要素を削除する。返り値はgpointerのGArray。
  2. g_array_remove_range(GArray* array, guint index_, guint length): arrayindex_番目から長さlength個分だけの要素を削除する。返り値はgpointerのGArray。
  3. g_array_remove_index_fast(GArray* array, guint index_): arrayからindex_番目の要素を削除するが、速度優先で、残った要素はarrayの時の順序を保持せず、位置が変わる事がありうる(だからあまり使われていない)。返り値はgpointerのGArray。
いずれにせよ、GArrayから「任意の要素を削除する」場合は、GArrayの要素の「ズラし」が必要になるんで、演算コストがかかる、と言う事は覚えておこう。


次はソートを見よう。
とは言っても、これもGListやGSList、あるいは素のC言語でのqsortの使い方と同じだ。気を付けるのは、比較関数をcallbacksGCompareFuncへとキャスティングする辺りだ。



他にはg_array_sort_with_dataがある。ラムダ式が無いC言語ならでは、の関数で、比較関数がGArray以外に引数を取る際に使用する。

これでGArrayの使い方は終了、だが、もう2つばかりGArrayの親戚がある。
それらを軽く紹介してこの記事は終了しよう。
1つ目はGPtrArrayだ。



GArrayは「バイトの塊」を指定していたが、GPtrArrayの場合、要素は名前が表してる通りポインタ(gpointer)となっている。
結果、事実上ポインタ塗れのGArrayを使うより、GPtrArrayの方が使い勝手はいいかもしんない。



  1. g_ptr_array_new(void): GPtrArrayのコンストラクタ。無引数。長さ1のGPtrArrayとして初期化する。返り値はgpointer型を要素とするGPtrArray。
  2. g_ptr_array_add(GPtrArray* array, gpointer data): arraydataをケツから追加する。dataはgpointer型(事実上void*型)なんで何でも指せる。破壊的変更目的のプロシージャなんで、返り値はない。
  3. g_ptr_array_foreach(GPtrArray* array, GFunc func, gpointer user_data): GPtrArray用の副作用目的の高階関数。arrayfuncを順次適用していく。funcの想定引数は第二引数まで、で第二引数に与えるデータがuser_dataになる。funcの引数が1つだけならuser_dataはNULLとなる。返り値はない。
  4. g_ptr_array_remove_index(GPtrArray* array, guint index_): arrayindex_番目の値を削除する。返り値はgpointer。
  5.  g_ptr_array_remove_range(GPtrArray* array, guint index_, guint length): arrayindex_番目からlength個の要素を削除する。返り値はgpointer。
  6. g_ptr_array_free(GPtrArray* array, gboolean free_seg): GPtrArrayの使用メモリを解放する。free_segがTRUEだった場合全部解放するが、FALSEだった場合はGPtrArrayと言うガワだけを解放する(って書いてるけどよう分からん・苦笑)。
上で書いた6つの機能はGPtrArray用だ、ってだけで、機能的にはGArray用のそれらと変わらん、って事が分かるだろう。



もう一つはGByteArrayだ。


GArrayはgchar*がdataだったが、GByteArrayは符号ナシのguint8*がdataになっている。


これも機能的にはいいだろ。結果、「符号付き」のバイトデータ(整数)を扱う場合はGArray、「符号なし」のバイトデータ(整数)を扱う場合はGByteArrayを使えば良い、ってのが仕様上の話、となる。



と言うわけで、GArray関連の話は以上、だ。
いずれにせよ、GLibを使う場合は、素のC言語の「配列」を使うよか、GArrayを使った方がいいし、素のCの「性的静的配列」もガンガンGArrayに変換して使っていこうじゃないか。

※1: 厳密に言うと、C99ではint8_t型はchar型へのエイリアスとなっていて、結果、8bit幅を表さない。
まぁ、ここでは「記述上の明快さ(何を目的にしてるのか)」を重要視する、って事にしよう。
ちなみに、バイトがあまりに曖昧なんで、人によっては8bit幅をオクテットと呼ぶ。
  • Xでシェアする
  • Facebookでシェアする
  • はてなブックマークに追加する
  • LINEでシェアする

最近の「プログラミング」カテゴリーもっと見る

最近の記事
バックナンバー
人気記事