Yabu.log

ITなどの雑記

C言語の復習その4。enum,3項演算子,goto,関数

関数ポインタをちょっと読み込んだくらい進みましたが、 関数ポインタは難しそうなので、それで一つ記事を書きます。

というわけでキリが良いので投稿♪

enum

  • enumのメンバは本来値を持たない。(持つべきではない)
  • 実は数値型の値を持っている。
    • 先頭から0,1,2,3...と割り振られている。
typedef enum
{
  SUCCESS, //0
  FAILURE  //1
}Result;
  • ただしこの値を直接計算・表示に使うのはenumの利用方法としてはふさわしくない。
    • K&Rにこの「ふさわしくない利用例」が乗ってしまっているらしい。
  • 確かJavaenumも内部ではintか何かだったが、内部の数値を直接利用できなかった気がする。
  • 残念ながら本来想定されていた用途で利用されていることは少ないらしい
    • C言語の仕事をしたことがないので分かりません。
typedef enum
{
  SUCCESS,FAILURE
}Result;

/**
 * print divided result.
 *
 * @param  dividend [description]
 * @param  divisor  [description]
 * @return          [description]
 */
Result divide(int dividend,int divisor){

  if(b==0){
    return FAILURE;
  }

  printf("calc result is %d\n",dividend / divisor);
  return SUCCESS;
}

int main(){
  Result result = divide(6,0);
  if(result == FAILURE){
    printf("divide is fail\n");
  }

  //enumのメンバは数値を割り当てられているが、それを表示したり処理に使うのはenum本来の使い方ではない。

  printf("success num is %d\n",SUCCESS); //0
  printf("failure num is %d\n",FAILURE); //1
}
  • OSのコードとして使うなら例外の種類を表現したりするのに使えるのではないかなと思いました。
    • トラップ
    • フォールト
    • アボート
typedef enum
{
  trap,fault,abort
}Exception;

コメントの指摘

Kaz Mu (id:fa11enprince)さんからこんな指摘をいただきました。

Javaと違いC言語のemumは単なるint型です。 C言語は弱い型付けなので普通にintのように扱えます。

OSを作る、読むという目標から、あまり重要でないと思い検証していなかったのですが、 せっかくコメントを頂いので検証してみます。

本書ではenumは整数型である必要はあるが、int型である必要は無いと明記されていました。

C言語の使用には「enum型の値は、整数としても使える」とはっきり定められています 注)これはあくまでも「整数」であって、「int」では無いことに注意してください。本書に付属のgccでは,enum型はこの部分はかなり合理的に最適化することができ、定数の数が255以下の場合には1バイト、それ以外の場合には2バイト取られるようにするオプションが用意されています。

少し調べたところ、-fshort-enumsというオプションがそれのようです。

http://www.keil.com/support/man/docs/armclang_ref/armclang_ref_chr1411640303038.htm を参考に検証するコードを書いてみました

enum defaultEnum{test,test1,test2};
// Largest value is 8-bit integer
enum int8Enum  {int8Val1 =0x01, int8Val2 =0x02, int8Val3 =0xF1 };

// Largest value is 16-bit integer
enum int16Enum {int16Val1=0x01, int16Val2=0x02, int16Val3=0xFFF1 };

// Largest value is 32-bit integer
enum int32Enum {int32Val1=0x01, int32Val2=0x02, int32Val3=0xFFFFFFF1 };

// Largest value is 64-bit integer
enum int64Enum {int64Val1=0x01, int64Val2=0x02, int64Val3=0xFFFFFFFFFFFFFFF1 };

//コンパイルできず
//enum int128Enum{test3=0x01,test4=0x02,int128Val=FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1}
/*
chap2/enum.c:22:49: error: integer literal is too large to be represented in any
      integer type
enum int128Enum{test3=0x01,test4=0x02,int128Val=0xFFFFFFFFFFFFFFFFFFFFFF...
*/   

printf("size of default    is %zd\n", sizeof (enum defaultEnum));
printf("size of int8Enum   is %zd\n", sizeof (enum int8Enum));
printf("size of int16Enum  is %zd\n", sizeof (enum int16Enum));
printf("size of int32Enum  is %zd\n", sizeof (enum int32Enum));
printf("size of int64Enum  is %zd\n", sizeof (enum int64Enum));
//printf("size of int128Enum is %zd\n", sizeof (enum int128Enum));
$ gcc chap2/enum.c
$ ./a.out
size of default    is 4
size of int8Enum   is 4
size of int16Enum  is 4
size of int32Enum  is 4
size of int64Enum  is 8

$ gcc -fshort-enums chap2/enum.c
$ ./a.out
size of default    is 1
size of int8Enum   is 1
size of int16Enum  is 2
size of int32Enum  is 4
size of int64Enum  is 8

検証の結果から手元のapple clangでは

  • enumのメンバはデフォルト4バイト
  • 4バイトで表現できない場合は8バイトになる
  • -fshort-enumsオプションをつけるとなるべく小さい値でenumコンパイルする
  • 値の代入がなければ4バイトが割り振られる

ということがわかりました。

3項演算子

int a =-1;
printf("%d\n",a);

//入れ子にできる
char *judge_result = 0!= (a>0? a: 0)? "正の数":"0か負の数";

//奇妙にみえるが以下と同じ意味

//int fixed_number = a>0? a: 0;
//char *judge_result = 0!= fixed_number ? "正の数":"0か負の数";

printf("%s\n",judge_result);

3項演算子は多分、式なので入れ子にできます。 可読性が無くなりますが。

  • 個人的に3項演算子と言うネーミングはいまいちしっくりこない。
  • if式と言う名前ならしっくりきてたかな。
    • Kotliのwhen式みたいな感じ
    • ?式、q式とかでもいいかな?
      • 逆にわかりにくくなってしまった。
  • osdev-jpで教えてもらいましたが、3項演算子というのは俗称で、JIS C99 規格書 § 6.5.15*1 では条件演算子と書かれているそうです

GOTO

  • gotoが有用な場合もある
    • 例えば2重ループから抜ける場合
    • 以下サンプル
/**
 * 九九を計算して表示する
 * 引数limitの数字に到達した時点で処理をやめる
 * @param limit [description]
 */
void nine_nine(int limit){
  for(int i=1;i<10;i++){
    for(int j=1;j<10;j++){

      int result = i*j;
      printf("%d\t",result);

      if(result==limit){
        goto last; //2重ループを一気に抜ける
      }

    }
    printf("\n");
  }
  last:
  printf("\n"); //ちなみにこの1行を抜くとコンパイルエラーになる
}

このコードをgotoをつかわずに書くとこんな感じになります。

/**
 * 九九を計算して表示する
 * 引数limitの数字に到達した時点で処理をやめる
 * @param limit [description]
 */
void nine_nine(int limit){
  for(int i=1;i<10;i++){
    int result;
    for(int j=1;j<10;j++){

      result = i*j;
      printf("%d\t",result);

      if(result==limit){
        break;
      }  
    }
    printf("\n");
    if(result==limit){
      break;
    }  
  }
  printf("\n");
}
  • まぁ抜けたい分のループだけ別の関数に括り出してreturnすると言うのもアリですが。
  • 疑問:GOTOのラベルは関数ブロックの最後尾にかけない?

    • 手元のGCCでは上記のGOTOを使った関数は最終行のprintf("\n");を抜くとエラーになった。
      • またまた某所で教えてもらいましたが、

      ラベルの次行に空文であるセミコロン ; を置けば、ブロックの最後尾にラベルを書くことが出来ます。

      • とのことです。検証したところ確かにコンパイル&実行できました。
  • gotoが移動できるのは同じ関数の中だけ。

    • near jumpみたいだなと思いました。
$ gcc chap4/control.c
chap4/control.c:25:1: error: expected statement
}
^

関数

気になる記述があったのでメモ。

実際には以下のようなことがあることは、実務を行なっているC言語プログラマでも知らない人もいるようです。 呼び出し側は返値は受け取らなくても良い 返値を返さない関数も定義できる 返値の型を省略できる場合もある 返値を受け取る側の変数と、返値の肩は必ずしも一致していなくても良い

P204より。C言語の仕事をしたことがないので(以下略)

  • printfは数値を返している
    • 成功した場合は表示した文字数を
    • 失敗した場合は-1を
int num = printf("test\n");
printf("%d\n",num);//5
  • 値を返さないのに関数?

数学的に考えれば、「関数」と言うのは、必ず結果(つまり返値)が導かれるものです。

  • この話は非常に納得がいく。
  • ちょっと昔、こういう細かい言い方にこだわってて関数っぽいものは以下のように言い分けるように心がけている

    • サブルーチン:戻り値を返さないこと
      • コマンドクエリ分離の原則(CQRS)でいうコマンドに対応するもの
    • メソッド:メンバに対する処理(参照更新代入等)があること
    • 関数:値を返すこと、副作用がないこと
  • int型の関数は型の宣言を省略できる

    • 引数も省略できる
test(a){
  return a+3;
}

・・・

printf("%d\n",test(5));//8
$ gcc --version
Configured with: --prefix=/Applications/Xcode.app/Contents/Developer/usr --with-gxx-include-dir=/usr/include/c++/4.2.1
Apple LLVM version 9.1.0 (clang-902.0.39.2)
Target: x86_64-apple-darwin17.7.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin

$ gcc chap5/function.c
chap5/function.c:8:17: warning: implicit declaration of function 'test' is
      invalid in C99 [-Wimplicit-function-declaration]
  printf("%d\n",test(5));
                ^
chap5/function.c:14:1: warning: type specifier missing, defaults to 'int'
      [-Wimplicit-int]
test(a){
^
2 warnings generated.

  • main関数の型をvoidにするのは間違い

    • C言語の仕様ではない。
    • int main(int args,char *argv[])がより良い。
  • externとstatic

    • externは他のファイルから呼び出せる
    • staticは呼び出せない。
    • extern,staticの指定がない場合はexternになる。
      • C++ではstaticになるらしい?
  • 関数のextern,staticはだいたい上記の認識でいいが変数は別。

fa11enprince.hatenablog.com

C言語能力が着実にレベルアップしています。かなり良い本だと思います。

プログラミング学習シリーズ C言語改訂版 2 はじめて学ぶCの仕組み

プログラミング学習シリーズ C言語改訂版 2 はじめて学ぶCの仕組み