第48回 補足 - 変数の振る舞い

今回は変数の扱いについて補足します。

目次

変数の復習

今までの記事で、変数の振る舞いについて詳しく説明していませんでしたので、この記事ではもう少し掘り下げます。

C++言語では以下の変数型が用意されていました。

また、上の型の名前とは別に、変数のサイズ(メモ帳の大きさ)を数字で表現した変数型もありました。

この入門シリーズではuint8_tなどの変数型で宣言しますが、個人的な印象ではArduinoのスケッチではintなどの型名を使われているケースの方が多いように思います。そのため、どちらの型でもスケッチを読めるようにしておいた方がいいかな、と思います。


ところで、変数型によって扱える数値範囲が異なりますが、そもそも数値範囲を超えたらどうなるのでしょうか?

Arduino IDEのことだから、また例のごとく大怒りしそうですよね。関数名をdigitalwriteのように一文字間違えただけでも大怒りするArduino IDEですよ!

それが、変数型の数値範囲を超えたら、それはもう大怒りするのではないでしょうか?

そこで、この記事では、あえて数値スケッチを作成して、変数の性質を調べていこうと思います。

変数型の数値範囲を超えたらどうなるか?

それぞの変数型で、数値範囲を超えたらどうなるのか、以下の2つの観点で振る舞いを調べてみたいと思います。

  1. 数値範囲をオーバーした場合どうなるか?
    例えばuint8_t型の変数は0〜255の範囲の数字しか扱うことができません。この変数に300を代入したり、「200 + 200」などのように結果が255を超えるような計算をするとどうなるのか調べてみたいと思います。
  2. プラスのみの数値範囲の変数にマイナスを代入したらどうなるか?
    uint8_t型の変数はプラスの数しか扱えませんが、−100などのマイナスの数字を代入したり、「100 – 200」などのように結果がマイナスになるような計算をするとどうなるのでしょうか?

実際にこれらのことを確認するスケッチを作成して、Arduino Microで試してみましょう。

❶ 数値範囲をオーバーした場合どうなるか?

まずはuint8_t型(0〜255)の変数に「300」を代入して、その変数をシリアルモニタに表示するスケッチを作成してみました。

/*
 * 変数の振る舞いを確認する
 *   uint8_t型の変数に300を代入するとどうなるか確認する
 */

void setup() {

  // シリアルモニタ準備
  Serial.begin(9600);
  while(!Serial){
  }

  // 変数宣言
  uint8_t test;

  // 変数testは0〜255なのに、300を代入してどうなるか試してみる
  test = 300;

  // シリアルモニタに変数testの値を表示する
  Serial.print("変数testの値: ");
  Serial.println(test);
  
}

void loop() {
  // loop関数では何もしない
}

uint8_t型の変数testを宣言して、test = 300;というように扱える範囲を超える数字を代入して、testの値をシリアルモニタでしてみます。

関数名を1文字間違えても怒るArduino IDEですので、これも怒られるんですかね…?

スケッチ検証ボタンでスケッチに問題ないか確認してみましょう。

あれ?

uint8_t型の変数に300を代入したら怒られるかと思ったら、「コンパイルが完了しました」と表示されて特に何も問題なくスケッチのチェックが終わりました。

これはかなりまずい状況です。

というのは、いつもちょっとしたことで怒っている人が、明らかに怒る状況なのに怒らない、というのは怒りを通り越して別の次元にいってしまった、ということなんです。この状況になると謝ってもどうにもならない状況、というのが私の人生経験上、確実に言えることです。

それはさておき、Arduino Microの中では何が起こっているのでしょうか?

上のスケッチはシリアルモニタに数字を表示するようにしていますので、シリアルモニタで変数testの値を確認してみましょう。

300を代入すると結果は44でした。これはどういうことなのでしょうか…?


もう少し詳しく調べるために、別のスケッチを作成して動作を確認してみます。

/*
 * 変数の振る舞いを確認する
 *   uint8_t型の変数に1ずつ足しながら値をシリアルモニタに表示する
 */

void setup() {

  // シリアルモニタ準備
  Serial.begin(9600);
  while(!Serial){
  }

  // 変数宣言
  uint8_t test = 0;

  while( true ){
    // シリアルモニタに変数testの値を表示する
    Serial.println(test);

    // testの値を1増やす
    test++;

    // 100ms待つ
    delay(200);
  }

}

void loop() {
  // loop関数では何もしない
}

このスケッチはuint8_t型の変数testを使って、1を足しながら結果をシリアルモニタに表示する、という内容です。シリアルモニタには次のよう0、1、2…と表示されるはずです。

0
1
2
3
4
5
6
7
.
.
.

変数testの範囲は0〜255ですので、255に1を足したら次はどうなるか気になりますので、その部分を確認すると…

255に1を足すと0になっています!

つまり、変数は1を足してその範囲を超えると最初に戻って0にしてしまうんです!

先程のスケッチでは、uint8_t型のtestに300を代入しましたが、これは、255を超えています。255より大きい数はどうなるかというと、256は変数testにとっては0、257は1、というように続けていくと300は44になります。

最初のスケッチの結果が44というのはこのような仕組みだったんです。

この例ではtest = 300;と直接300を代入しましたが、次のように255を超える計算をしても、超えた分は0からカウントされます。以下の計算結果も44になります。

uint8_t test;  // 0〜255の範囲

test = 100 + 200;  // 100+200 = 300を代入することになるが、結果は44

uint8_t型の変数に300を代入してもArduino IDEは怒りませんでした。

最初に冗談で「普段ちょっと怒る人が怒らないのは怖い」と書きましたが、実は変数の数値範囲を超えることは本当に怖いことなんです!

変数の数値範囲を超える計算が出てくる場合、Arduino IDEは怒りません。でもあとでスケッチを動作させたときに思わぬ不具合が出てくることがあるんです。

この変数の数値範囲超えについては、最後にミニチャレンジ課題を用意しましたので、ご興味があれば考えてみてください。

❷プラスの範囲の変数にマイナスを代入したらどうなるか?

次に、uint8_t型の変数(0〜255のプラス整数)にマイナスの数値を代入してどうなるか確認してみます。

次のようなスケッチを作成してみました。

/*
 * 変数の振る舞いを確認する
 *   uint8_t型の0〜255のプラス整数を扱う変数にマイナスの数値を代入する
 */

void setup() {

  // シリアルモニタ準備
  Serial.begin(9600);
  while(!Serial){
  }

  // 変数宣言
  uint8_t test = -1;

  // シリアルモニタに変数testの値を表示する
  Serial.print("変数testの値: ");
  Serial.println(test);

}

void loop() {
  // loop関数では何もしない
}

このスケッチはuint8_t型の変数test-1を代入してその値をシリアルモニタで確認する、という内容です。

先程の数値範囲オーバーの時と同じく、スケッチの検証をしてもエラーにはなりません。

このスケッチをArduino Microボードに送ってシリアルモニタで結果を確認すると、次のようになりました。

またまた不思議な結果になりました。

「-1」を代入したのに結果は「255」です。なぜこのような結果になるのか、その背景を確認しておきましょう。

符号なし(プラスのみ)のuint8_t型と、符号あり(プラスもマイナスもOK)のint8_t型は両方とも256個の数字を扱うことができます。

uint8_t型の変数もint8_t型の変数も、 256個の数値を扱うことができるようになっています。この256個の数値について、次のような256個のマスで考えていきます。

uint8_t・int8_t型変数

最初は符号なしのuint8_t型の変数です。

この型の変数は、256個のマスに0〜255の数字を割り当てています。

uint8_t型変数

256個のマスを0から255に割り当てる、というのは当たり前といえば当たり前ですよね。


次は符号ありのint8_t型の変数です。

この型の変数は、256個のマスに-128 〜 +127の数字を次のように割り当てています。

int8_t型変数

ちょっと並び方が変な気がしますよね。この理由を説明するには「2進数」というコンピュータ内部の理解が必要ですので、ここではこのようになっている、と理解しておいていただければと思います。


uint8_t型とint8_t型の数字の割り当てをくらべると、次のようになります。

uint8_tとint8_tの対応

0〜127の数字に関しては、uint8_t型もint8_t型も意味は同じですが、uint8_t型の128〜255は、int8_t型ではマイナスの数字の意味になってしまうんです。

つまり、uint8_t型の変数に「-1」を代入しても、uint8_t型にとっては「255」という意味なので、先程のスケッチではシリアルモニタに「255」と表示されてしまったわけです。

同様に、例えば変数uint8_t test = -3;というようにuint8_t型に「-3」を代入すると、253という数値に解釈されてしまいます。


このように、変数型の数値範囲を超える数値を代入してもエラーにはならず、勝手にその変数型の数値に解釈されてしまいますので、使用する変数型について意識するようにしていただければと思います。

使用する変数型

最後に、Arduino IDEのサンプルスケッチなどでは変数型をどのように扱っているのか確認しておきます。

Arduinoの公式サイトにはArduinoの命令、関数のマニュアルがあります。

そのマニュアルにはサンプルスケッチが掲載されていますが、多くのスケッチではint型の変数が使用されています。

公式サイトの変数型はint型なので、int型を使うことが推奨されているのかな、と思ってしまいますよね。そこでスケッチで使用する変数型について補足します。

個人的な主観もありますが、次のように考えていただければ大きな問題はないと思います。

  • 普通に変数を使用する場合はint型で問題なし
  • スケッチ内容によっては変数型を意識する

この考え方についてもう詳しく説明します。

普通に変数を使用するint型で大きな問題なし

このシリーズで使用しているuint8_t型は、扱える数値範囲が狭いので充分注意する必要があります。

例えば0から500のカウントをしたいのにuint8_t型を使うとスケッチが思ったように動作してくません。

でもいちいち変数型と数値範囲を気にするのは正直なところ、ちょっと面倒ですよね。


そこで、変数を宣言するときには深く考えずにint型を使用すれば、「-32768 〜 32767」の数字を扱えるので、普通の使い方であれば大きな問題はなさそうですよね。

ということは、「int8_t」型や「uint8_t」型など、間違えると問題が起きる変数型を使うよりも、全部「int」型にしてしまった方が楽だし安全ですよね。

このような背景から、Arduinoの公式サイトに掲載されている参考スケッチは、その多くがint型の変数を使用して、より問題が発生しないようなスケッチにしているようです。

スケッチ内容によっては変数型を意識する

とはいっても、どのようなときでもint型使うと問題が発生することがあります。

そこで、変数型を意識する必要があるケースについてご紹介します。


例えば、次のように湿度センサーで測定したデータ(0〜100%の整数数値)を記録するために配列を用意するケースを考えてみます。

このとき、測定したデータを記録するための配列を宣言する場合、変数型をint型とuint8_t型の2種類で比較してみます。

// 湿度センサーの値を記録する配列 ... int型配列の場合
int humidity_int[2000];  // この場合、メモリサイズは2000 x 2 = 4000バイト必要

// 湿度センサーの値を記録する配列 ... uint8_t型配列の場合
uint8_t data2[2000];  // 2000 x 1 = 2000バイト必要

ここで、それそれの配列が必要なメモリサイズを計算してみます。

int型はメモリを2バイト使用します(Arduino Microの場合)。一方でuint8_t型は1バイト使用します。

上のスケッチのコメントに書いてあるように、int型の場合は4,000バイト、uint8_t型の場合は2,000バイトのメモリサイズが必要になります。

この例では配列に保存するデータは湿度ですので、0〜100の範囲になります。

つまり記録するためにはuint8_t型の配列で十分ですよね。

int型では「-32768 〜 32767」の範囲を扱えますが、湿度データを保存するにはちょっともったいないですよね。


ところで、メモリサイズは有限で、Arduinoボードの種類によってはかなり限られているケースがあります。

例えば、Arduino Microの場合、メモリは2560バイトしかありません。

つまり、上のスケッチでint型の2000個の配列を用意した場合、すべてのデータを記録できないということになってしまいます。

でもuint8_t型の2000個の配列は用意して使うことができます。

このように多くのデータを扱う場合は、変数型を意識しないとArduinoの能力を十分に活用することができなくなってしまうケースもありますので、こういうこともあるんだ、と頭の片隅に置いていただければと思います。

このシリーズでは、扱える数値範囲が狭いuint8_t変数型を使用していますが、変数型の理解を深めるためにint型は使用せず、ちょっと面倒なuint8_t型などを使用しました。

ミニチャレンジ課題

変数の数値範囲について、問題が発生するスケッチを修正してみましょう!

次のスケッチは1秒に1回LEDを光らせて、5分経過したらブザーを鳴らすスケッチです。残念ながらこのスケッチはLEDの点滅を永遠に繰り返してブザーが鳴ることはありません。

どのように修正すれば良いでしょうか…?

/*
 * うまく動作しない5分タイマー
 *    スタートボタンを押してタイマーをスタートすると
 *    時間になったらブザーが鳴るはずが、
 *    永遠にブザーが鳴らないスケッチ
 */

// 秒表示LEDのピン番号
#define PIN_BYOU 12

// スピーカーのピン番号
#define PIN_SPEAKER A0

// スイッチのピン番号
#define PIN_SWITCH A5

void setup() {
  // LED接続ピンの設定
  pinMode(PIN_BYOU, OUTPUT);

  // スピーカー接続ピンの設定
  pinMode(PIN_SPEAKER, OUTPUT);

  // スイッチ接続ピンの設定
  pinMode(PIN_SWITCH, INPUT_PULLUP);

  // LEDを消しておく
  digitalWrite(PIN_BYOU, LOW);
}


void loop() {
  // スタートスイッチが押されるまで待つ
  while( digitalRead(PIN_SWITCH) == 1 ) {
  }

  //  タイマー時間5分 = 60秒 x 5 = 300秒カウントする
  for( uint8_t i=0; i<300; i++ ) {
      digitalWrite(PIN_BYOU, HIGH);
      delay(50);
      digitalWrite(PIN_BYOU, LOW);
      delay(950);
  }

  // 時間になったらアラーム音を3回鳴らす
  for( uint8_t i=0; i<3; i++) {
    tone(PIN_SPEAKER, 440);
    delay(60);
    noTone(PIN_SPEAKER);
    delay(60);
    tone(PIN_SPEAKER, 440);
    delay(60);
    noTone(PIN_SPEAKER);
    delay(60);
    tone(PIN_SPEAKER, 440);
    delay(100);
    noTone(PIN_SPEAKER);
    delay(900);
  }

}

更新履歴

日付内容
2020.1.25新規投稿
2021.8.27新サイトデザイン対応
2022.2.18誤字訂正
2025.1.19説明内容一部変更
通知の設定
通知タイミング
guest
0 コメント
新しい準
古い順 一番投票が多い
本文中にフィードバック
全てのコメントを見る
目次