初老のボケ防止日記

おっさんのひとりごとだから気にしないようにな。

ご無沙汰のC

「はじめてのC」どころかもう久しくCなんてしてねえよという倦怠期のオジサンです。ここ数年はJavaやらPythonやらで開発していたので久しぶりにC言語のプログラムを弄ることになって感じたことを記事にしました。題して「ご無沙汰のC」

俺の知ってるCと違う

はじめてのC。それはもう15年以上前…最後にCを触ったのも20世紀の話で、21世紀になってはVC++とかはやった気がするけど純粋なC言語は使ってなかった気がします。で、いつの間にかC99とかいう規格になっていたわけで、規格としては2000年位にANSI標準になったみたいだけど、ネットで記事を探して見る限り普及したのは10年位前の2005年前後なんかなーとか。。
アダルトなC(C89)とナウなヤングのC(C99)の違いはここにわかりやすくまとまってる。

C言語の最新事情を知る: C99の仕様 - Build Insider

C++とかGCCからいいところを取り入れた感じで使い勝手がよくなっているぽい。でも、C89のままでもそのまま動くので無理してC99の新機能を使う必要もないし、そもそも今の時代に好んで新規開発でC言語を採用するケースは自分の場合、限られるので、今まで全くみてなかった。いや、正直に言うと見ないふりをしていた。だって文字列処理とか面倒臭いじゃん、C言語で。Javaの方が標準パッケージで色々とできるしコレクションクラスとかもあるじゃんと。でもC言語で開発されたシステムを改造する場合はC言語なんだよね。だからC言語嫌いだから使わないとかいってられないんだってばよ。

ということで、軽く触ってみて気に入ったところだけまとめてみる。

C++ぽい記述

この変はもともとGCCとかならできてた記憶だけど、ANSI標準でできるようになったのはよいこと。

  • const使える
  • inline使える
  • 行コメント(//)使える
  • 変数宣言を関数冒頭じゃなくてもできる

新しい型

型が増えたのはよい。覚えるの面倒くさいけど。

ブーリアン型(stdbool.h)

他の言語でお馴染みBOOL。昔はよくdefineで無理やりBOOLとか作ってた気がする。因みにサイズは1byte。"stdbool.h"をインクルードして使うのね。

整数型(stdint.h)

諸々増えた。といってもLinuxではtypedefで定義されているらしい。

C89 C99 size(byte)
char int8_t 1
short int16_t 2
int int32_t 4
long int64_t 8
unsigned char uint8_t 1
unsigned short uint16_t 2
unsigned int uint32_t 4
unsigned long uint64_t 8

でもこれだけでタイピングが減るし、何よりもサイズが型名でわかるのでよろしい。あと、処理環境問わずサイズが変わらないのもよい。というのは、C89の変数だと32bitと64bitで変数のサイズが変わってしまう。それの何が困るかというと、例えば、C言語で作ったプログラムとPythonでctypesでバイナリフォーマットでやりとりするときにビルド環境によってサイズが変わる変数があるのでおかしくなってしまうのだ。

64bit OS と 32bit OS でのデータ型の相違一覧 - drk7jp

って、"long"と"long double"だけなんだけどね。

フレキシブル配列メンバ

実はこれが一番びっくりした。というか最初見て理解できなかったので簡単な例をあげてみる。

こんなデータ構造があるとする。いわゆる固定長のヘッダ部と可変長のデータ部から構成されるデータ構造。通信とかファイルのデータ構造でありがちなやつ。

ID(8byte)
バージョン(8byte)
データ長(16byte)
可変長データ部(データ長分のサイズ)

普通はこんな感じで固定長部分だけ構造体として定義すると思う。

固定長部分だけ定義(C89)

typedef struct s1 {
  uint8_t  id;
  uint8_t  version;
  uint16_t length;
} S1;
int main(){
  printf("struct S1 size:%lu \n", sizeof(S1));
  uint8_t  buf[16];

  S2 * s2 = (S2 *)buf;
  s2->id = 2;
  s2->version = 1;
  s2->length = 4;
  memset( s2->data, 0xff, s2->length);
  int i;
  for( i = 0; i < sizeof(buf); i++ ){
    printf("%02X", buf[i]);
    if( (i +1) % 8 == 0 ){
       printf("\n");
    }
  }
  printf("\n");
}
struct S1 size:4 
02010400FFFFFFFF
0000000000000000

構造体はヘッダ部だけ定義してあるのでサイズは8byte、可変長データ部にポインタをずらすには構造体のヘッダサイズ分だけポインタを進めればいい。こういう時はC言語様は素晴らしい。なんでもポインタ演算すればいいんだもの。メモリ破壊万歳。

で、これをC99でやるとこうなる。

固定長部分だけ定義(C99)

typedef struct s2 {
  uint8_t  id;
  uint8_t  version;
  uint16_t length;
  uint8_t  data[]; // フレキシブル配列メンバ
} S2;
int main(){
  printf("struct S2 size:%lu \n", sizeof(S2));
  uint8_t  buf[16];

  S2 * s2 = (S2 *)buf;
  s2->id = 2;
  s2->version = 1;
  s2->length = 4;
  memset( s2->data, 0xff, s2->length);
  int i;
  for( i = 0; i < sizeof(buf); i++ ){
    printf("%02X", buf[i]);
    if( (i +1) % 8 == 0 ){
       printf("\n");
    }
  }
  printf("\n");
}
struct S2 size:4 
02010400FFFFFFFF
0000000000000000

結果は全く同じ。構造体のメンバに要素数を省略した配列メンバを追加しているのに構造体のサイズが変わらない。こいつはフレキシブル配列メンバといって、構造体の最後にだけ定義できる特別扱いなメンバらしい。特待生か。しかもサイズは最初の構造体と同じ。つまりこのメンバはサイズが「0」なんである。幽霊か貴様は。これの何が便利かというと、sizeof演算子では固定部長が正しく取得できる上に、可変長データ部を示すポインタもそのまま使えるという訳。

以下の定義でも同様の効果があるようだけど、久しぶりにC言語のコールドリーディングしたらポインタが定義されていると勘違いしてしまって、構造体のサイズがおかしいのにまともに処理が動いていると勘違いして混乱した。

typedef struct s3 {
  uint8_t  id;
  uint8_t  version;
  uint16_t length;
  uint8_t  data[0]; // これもフレキシブル配列メンバと同じになるっぽい
} S3;

Cも進化してる

ということで、もう少ししたら現場に新入が入社してきて、C言語研修には昔バリバリCをやったオッサンがあてがわれる事もあるかと思うんだけど、せっかくだから今の時代のCを教えてあげないともったいないなと思った訳。ちなみに今はC11というのが最新なんだせ? 困ったもんだ。

C11 (C言語) - Wikipedia


苦しんで覚えるC言語

苦しんで覚えるC言語