the ド田舎エンジニア

東京とは名ばかり 八王子市は尾崎 幸一朗(通称:サチロー)による気まぐれブログです

【C言語】汎用ポインタの利便性

サチローこと幸一朗です。

汎用ポインタについて、使い道がわかりませんと、質問を受けたため、自分なりに解説。

【大前提】

通常、変数は型の整合性を取らなければならない。

int i = 128;
char c;

c = i;
printf("%d\n", c);

これの実行結果は「-128」となる(はず)。
charは1バイトの変数なので、「11111111」が最大値となります。
が、最上位のビットは符号ビットとなり、正の値では「01111111」が最大値。
つまり127までしか保持できない。

そこに対して4バイト変数を代入したため、オーバーフローが発生したという例。
※ちなみに、コンパイラによっては警告すら出さない。

代入元の型と、代入先の型は一致しているべきである。

【ポインタの扱い】

ポインタ変数は、変数が確保している領域の先頭アドレスを保持する。

int i = 128;
int *ip = &i;

printf("i[%p]\n", &i);
printf("ip[%p]\n", ip);

表示結果は一致する。

変数が領域を確保することは、ポインタ変数も同様。
では、下記の結果はどうなるか?

typedef struct {
  char foo;
  char bar;
} hoge;

printf("%d\n", sizeof(char));
printf("%d\n", sizeof(int));
printf("%d\n", sizeof(hoge));
printf("%d\n", sizeof(char *));
printf("%d\n", sizeof(int *));
printf("%d\n", sizeof(hoge *));

実行環境によって異なるが、
1
4
2
8
8
8
となる。

ポインタ変数では変数の先頭アドレスを格納するため、そこに格納されている値に依存しない。

【汎用ポインタ】

ポインタ変数同士であれば、アドレスを格納する点では共通しているが、問題は中身を参照する場合。

typedef struct {
  char foo;
  char bar;
} hoge;

hoge st;
int *p = &st;

printf("%d\n", p->foo);

この場合、「foo」という子は知りませんと、コンパイルエラーが発生する。


ここでvoid型のポインタの登場。

typedef struct {
  char foo;
  char bar;
} hoge;

hoge st;
void *p = &st;
hoge *pst;

pst = (hoge *)p;

printf("%d\n", pst->foo);

一旦、void型のポインタに退避し、後からキャストすることで中身を参照することができる。

【汎用ポインタの利便性】

例えばこんな構造

typedef struct {
  int key;
  int value;
} token;

const token token_list[] = {
  { 1, 10 },
  { 2, 11 },
  { 3, 12 },
};

token型の配列を宣言。
keyを渡して値を得るhashのような使い方をしたい。

がしかし、この形式で管理できる値はint型に限定される。
void型のポインタにすることにより、これが回避される。

typedef struct {
  int key;
  void *value;
} token;

typedef struct {
  int foo;
  char bar;
} hoge

int i = 10;
char c = 'c';
char str[128 + 1] = "fuga";
hoge hoge_st;

const token token_list[] = {
  { 1, &i },
  { 2, &c },
  { 3, str },
  { 4, &hoge_st },
};

値を取り出したい場合、値を取り出した先で、同じ型のポインタ変数で受ければ良い。

/*
 tokenの最後尾を知る手段として、末端のkeyにそれを示す値が格納されているものとする。
 それが無理ならsizeをfetch関数に渡す。
*/
#define GetValue(type, key, list)    (type *)fetch(key, list)

void *fetch(int key, token *list)
{
  void *ret = NULL;

  while (list->key != KEY_TAIL) {
    if (list->key == key) {
      ret = list->value;
      break;
    }
  }
  return ret;
}      

int *ip;
hoge *hoge_p;

ip = GetValue(int, 1, token_list);
hoge_p = GetValue(hoge , 4, token_list);

といった具合に、型に依存せずにデータを管理できる。


型に依存しないという点では便利ですが、乱用は避けてください。
中身に何が入っているのか訳が分からなくなり、メンテナンス時に苦労します。

久しぶりのC言語 その4

こんばんは。
サチローこと幸一朗です。


C言語の方に戻って参りました。
さて、前回は第一関門の『配列』『ポインタ』について軽く触れていきました。


①配列を用いて以下を標準出力してください。
「1 5 103 65 33」

int array[5] = { 1, 5, 103, 65, 33 };

for (int i = 0; i < 5; i++) {
  printf("%d ", array[i]);
}
printf("\n");

以下の方法を取った方が修正範囲が少なくて済みます。
運用上はこのように使用します。

①文字定数を使用する

#define SIZE (5)

int array[SIZE] = { 1, 5, 103, 65, 33 };

for (int i = 0; i < SIZE; i++) {
  printf("%d ", array[i]);
}
printf("\n");

②sizeofで配列の要素数を取得する

int size = sizeof(array) / sizeof(array[0]);

②は、配列全体のサイズ ÷ 1要素のサイズから、配列全体の要素数を割り出しています。


②標準入力から文字列を得てください。その文字列において、文字「a」を検出した場合は「hit」、全く検出しなかった場合は「no hit」を1度だけ標準出力してください。

char str[128 + 1] = { '\0' };
int size = 0;

fgets(str, sizeof(str), stdin);

size = strlen(str);
for (int i = 0; i < size; i++) {
  if (str[i] == 'a') {
    printf("hit\n");
    break;
  }
}

if (i == size) {
  printf("no hit\n");
}

ベタっと書くとこんな具合ですが、普通はstrchrを使うべきでしょう。

char *p = strchr(str, 'a');
if (p) {
  /* p == 'a'を検出したaddress */
  printf("hit\n");
} else {
  /* p == NULL */
  printf("no hit\n");
}

③標準入力から得た文字列を反転して標準出力してください。
標準入力までは②と変わらないので省略。
forの部分を修正するのみでできます。

int size = strlen(str);
for (i = size - 1; i >= 0; i--) {
  printf("%c", str[i]);
}

注意すべきは、size=文字の長さなので、配列の添え字に使用する場合の最大値はsize - 1となること。

char str[128 + 1] = "abcde";

int size = strlen(str);
/*
 sizeは「5」だが、'e'を参照する場合はstr[4]
*/

例題の場合は、最後尾に'\0'が入るが、入っていない場合はアクセス違反となり落ちる。
「セグメンテーション違反」とか、「segmentation fault」、「例外が発生しました」とか表示される。

④変数「i」を宣言し、そのアドレスを標準出力してください。

int i = 0;

printf("%p\n", &i);


⑤変数「i」のアドレスをポインタ変数「p」に格納し、ポインタ変数を用いて変数「i」のアドレスを標準出力してください。
④のiのアドレスを変数に格納するだけ。
注意すべきは、ポインタ変数と、そこにアドレスを代入する変数の型を合わせること。

int i = 0;
int *p = &i;

printf("%p\n", p);

⑥「⑤」で使用したポインタ変数「p」のアドレスをポインタ変数「pp」に格納し、ポインタ変数「p」のアドレスを標準出力してください。
⑤の続き。

int i = 0;
int *p = &i;
int **pp = &p;

printf("%p\n", *pp);

意外と長くなってしまいましたね。
実際にコードを組んで、中身を標準出力し、中でどういった動きになっているのかを実際に見た方が理解しやすいと思います。


次回は関数について触れていこうかと思います。

長々と閲覧頂きありがとうございます。

Pythonが人気を浴びる理由

こんばんは。幸一朗です。

箸休めがてらにpythonの話。

ここ数年、やたらと人気なpython
パッと見た感じでは、そんなに難しくもなく、誰でも触れるような仕様なのです。
が、何故そこまで人気が出ているのかは理解できていませんでして。

話題になっているから触ってみようと。
数年前にもチラっと扱ってみた事があるのですが、日曜大工程度です。


うーむ。
これは確かに使いやすいかも。


まず、libraryが充実しすぎて簡単な構文で出来ることが多い。
その1つとして『辞書型』

これが辞書型の例

dict = { "yamada":85, "tanaka":70, "suzuki":95 }

『名前』対する『値』を持つ。
この値を取り出したい場合は、以下の構文。

val = dict["yamada"]

出力結果は『85』となる。
わずか数行。

これをC言語でやろうとすると

typedef struct {
  char name[64 + 1];
  int value;
} NameValue;

const NameValue name_val_list[] = {
  { "yamada", 85 }.
  { "tanaka", 70 }.
  { "suzuki", 95 }.
  { NULL, 0 }.
};

int fetch(char *name, const NameValue *list )
{
  int i = 0;
  int ret = -1;

  while (list[i]->name) {
    if(strcmp(list[i]->name, name)) {
        ret = list[i]->value;
        break;
    }
    i++;
  }
  return ret;
}

int main(void)
{
    char name[64 + 1] = "yamada";

    printf("%d\n", fetch(name, name_val_list);
    
    return 0;
}

コンパイルしていないので動くかはさておき、こんな感じになるはず。
書くだけでも面倒でした。ステップ数の違いは一目瞭然。

json形式のデータとも相性が良く、データ操作はすこぶる楽。


そして、ブロックが『インデント』なのも特徴的。

for文の例

for val in range(1, 5):
    print(val)

結果は
1
2
3
4
5
となる。

'{'で括る形式の場合、先頭に'{'と終端に'}'があれば基本的には動くため、その中の記述は大げさに言えば好き勝手に書くことができるが、pythonはできない。

誰が書いたとしてもクセの無いコードになりやすい。


書けば書く程、この仕様にはハマる。
是非お試しあれ。

久しぶりのC言語 その3

こんばんは。
サチローこと幸一朗です。

久しぶりのC言語といいつつ、最近はちょくちょく触っているためか久しぶり感がなくなってきました(笑)

それにしても難しい。
教えるのは難しい。
多分向いてない。

今回は配列、文字列、ポインタ。
C言語における難所の一つ。

配列とは、同じ型を持つ複数のデータを管理するものです。

#include <stdio.h>

int main(void)
{
  int array[5] = { 1, 2, 3, 4, 5 };
  int i = 0;

  for (i = 0; i < 5; i++) {
    printf("%d", array[i]);
  }
  printf("\n");

  return 0;
}

表示結果は『12345』。

array[x]のxは、x番目の要素を参照するもの。

厳密には、arrayという変数が確保しているメモリの先頭アドレスからsizeof(array[0]) * xした値を加算したアドレスを参照する(で合ってるかな?)もの。

先頭を参照する場合は「0」。
以降「1」、「2」・・・と指定します。
要素の数以上の値を指定するとエラーが起きます。
arrayという変数が確保していない(範囲外)のアドレスを参照することになるためです。


そして、この形式で表現されるのが、文字列。
C言語には文字列が存在しません。

#include <stdio.h>

int main(void)
{
  char str[] = "hoge";

  printf("%c\n", str[0]);

  return 0;
}

表示結果は『h』となる。
"hoge"は文字の配列なのです。

ちなみにstrの中身はこう。
str[0]:'h'
str[1]:'o'
str[2]:'g'
str[3]:'e'
str[4]:'\0'

'\0'は終端を示す。
こいつが無いとC言語は文字の終わりを理解できない。


そして配列を学び始めると「アドレス」という単語がボコボコ出てくる。
そこで、いよいよ「ポインタ」の登場です。

ポインタとは、変数のアドレスを格納する。
変数のアドレス?何のこっちゃ?

変数を使用するには領域が必要です。
ハードディスクにいろんなデータを突っ込んでいくと容量を消費するように、変数も値を記憶しておくためにメモリを使用します。

int a = 0;

printf("%p\n", &a);

みたいなことをすると、aという変数がどこのアドレスに割り当てられているかを確認することができます。

ポインタ変数を使うとこんな感じ。

int a = 0;
int *p;

p = &a;

printf("%p\n", p);

 // ポインタ変数に「*」を付けると、アドレスに格納されている値を得る
printf("%d\n", *p);


ポインタ変数のアドレスもポインタ変数に格納できる。

int a = 10;
int *p;
int **pp;

p = &a;
pp = &p;

printf("%p\n", &pp); // 変数「pp」のアドレス
printf("%p\n", pp);  // 変数「p」のアドレス
printf("%p\n", *pp);  // 変数「a」のアドレス
printf("%d\n", **pp);  // 10


これの使いやすさを理解するには実際にコードを組む経験を積んだ方が早いです。


これらを踏まえて、こんな課題を出してみました。

①配列を用いて以下を標準出力してください。
「1 5 103 65 33」

②標準入力から文字列を得てください。その文字列において、文字「a」を検出した場合は「hit」、全く検出しなかった場合は「no hit」を1度だけ標準出力してください。

③標準入力から得た文字列を反転して標準出力してください。

④変数「i」を宣言し、そのアドレスを標準出力してください。

⑤変数「i」のアドレスをポインタ変数「p」に格納し、ポインタ変数を用いて変数「i」のアドレスを標準出力してください。

⑥「⑤」で使用したポインタ変数「p」のアドレスをポインタ変数「pp」に格納し、ポインタ変数「p」のアドレスを標準出力してください。


概ねこんな感じの出題となりました。
未だに手探りです(笑)

久しぶりのC言語 その2

こんばんわ。
サチローです。

前回に引き続き、C言語です。

①以下の文字列を標準出力してください(標準出力)
 「hello world
最初の一歩は鉄板の「とりあえず表示してみる」ってやつです。

int main(void)
{
  printf("hello world\n");

  return 0;
}


②以下の整数、または文字を変数に格納し、格納結果を標準出力してください(代入)
・100
・a
変数。頻繁に使いますね。

int main(void)
{
  int i = 100;
  char c = 'a';

  printf("i[%d]\n", i);
  printf("c[%c]\n", c);

  return 0;
}


③標準入力から得た値を標準出力してください(標準入力)
実務ではscanfは非推奨。fgets、fscanfを使いましょう。

int main(void)
{
  int i = 0;

  scanf("%d", &i);

  printf("i[%d]\n", i);

  return 0;
}


④1から100までの整数を全て加算した結果を標準出力してください(for)
シンプルなコードを書くためにもloop文は必須ですね。

int main(void)
{
  int i;
  int ans = 0;

  for (i = 1; i <= 100; i++) {
    ans = ans + i;
  }

  printf("ans[%d]\n", ans);

  return 0;
}


⑤1から100までの整数のうち、7で割り切れる値のみ標準出力してください(if)
forとifの組み合わせ。

int main(void)
{
  int i;

  for (i = 1; i <= 100; i++) {
    if (i % 7 == 0) {
      printf("%d\n", i);
    }
  }

  return 0;
}


⑥下記を標準出力してください(for二重)
 ・・・・・
 ・・・・・
 ・・・・・

int main(void)
{
  int i;
  int j;

  for (i = 0; i < 5; i++) {
      for (j = 0; j < 5; j++) {
        printf("・");
    }
    printf("\n");
  }

  return 0;
}


コンパイルした訳ではないので、ちょっとしたミスはあるかもしれませんが、ロジックはこんな感じです。

次回の講義用の問題は作り途中です。
どういう順番でやったらわかりやすいかを合間見て整理してます。

久しぶりのC言語

サチローです。

先日、客先へ伺ったところ、「新人にCを教えてやってくれまいか?」とまさかの依頼がありまして。
飲みの席での依頼だったため、冗談かと思えば、本気らしい。

十年近くもなる顔なじみからの依頼。
さすがに断り切れませんでした。


さて参った。教えるのは苦手。
特に全く知識がない方へとなると、どこから教えて良いかさっぱりわからん。

しかもJavaの経験があるらしい。
じゃあJavaやらしてあげてよ(笑)
と思ったが、殆ど無に等しいとのこと。


私が教わった通り、まずはCの歴史を教えることに。
※気になる方は、「デニス・リッチー」でググってください。


そしてまずはお決まりの「hello world

#include <stdio.h>

int main(void)
{
  printf("hello world\n");
  
  return 0;
}


ついでにJavaだとこんな感じ(だった気がする)

public class Hoge {
  public static void Main(String[] args){
    System.out.println("hello world");
  }
}

perlpythonはよっぽど楽

print "Hello world!"


構文の説明は後回しにして、
プリコンパイル
コンパイル
リンク
の説明。

どういう仕組みで動いているのかを理解する事が大切でして。
「#includeは、おまじない」なんて話も聞きますが、実にとんでもない。
そもそも「おまじない」レベルの無駄な構文はプログラムに書くべきではありません。

とは言え、一回で理解できたら中々のもの。


一先ず、一通りの構文を教えたところで課題を作ることに。


①以下の文字列を標準出力してください(標準出力)
 「hello world

②以下の整数、または文字を変数に格納し、格納結果を標準出力してください(代入)
 ・100
 ・a

③標準入力から得た値を標準出力してください(標準入力)

④1から100までの整数を全て加算した結果を標準出力してください(for)

⑤1から100までの整数のうち、7で割り切れる値のみ標準出力してください(if)

⑥下記を標準出力してください(for二重)
 ・・・・・
 ・・・・・
 ・・・・・


大学の講師になった気分です。
これの後は配列、文字列、ポインタの講義をします。

少し楽しくなってきました。(笑)

マインクラフトを数年ぶりにやってみた

こんばんは。
サチローこと幸一朗です。

よくサブディスプレイにYouTubeを垂れ流し、BGM代わりにして仕事をしているのですが、なぜかマインクラフトの動画にありついていまして。

ブラウザバックで戻ってみたら、お笑いコンビのよゐこ濱口優さんにたどり着きました。
何かの企画でよゐこのお二人がマインクラフトでまるでコントのようなサバイバル生活。私ももう数年前になりますが、マインクラフトにはまった時期がありまして。仕事を忘れて見入ってしまいました。


全然ちがうやん。

肉マークなんてなかったやん。
馬おるやん。
木が黒いやん。
インベントリもようわからん。

こち亀の日暮になった気分。


一応、アカウントはメモってあったので久しぶりに立ち上げようとしたら、即難所。
Launcherって何だよ。。。。


最後に触ったのがβ1.いくつかだった気が。
完全なる浦島太郎。
何とか最新versionをもってきていざサバイバル。


そして島。
島スタートである。
イカとったどー!やれってか。


ともかく掘れ。
土を掘れ。
生後数秒で野宿の準備。
これもマインクラフト。


この世界では骨が矢を打つ。
この世界では蜘蛛が飛んでくる。
この世界では黒いよくわかんないやつがワープしてくる。

これらから逃れるために、一刻も早く屋内に退避する必要があるのだ。


Wikiを小窓に何とか拠点を作る。
雨風をしのぎ、収納と作業台、かまどがあればそれでよい。


仕事の合間みてまたハマってみようかなと。