Arduino – ESP32 の関数の参照渡し、ポインタ渡し、String渡しについて考えてみた

ESP32 ( ESP-WROOM-32 )

新年
  あけまして
    おめでとうございます。

旧年中は、いろいろご支援くださり、おかげ様でもうちょっとこのブログ運営がんばっていこうと思います。
本年も何卒よろしくお願いいたします。
m(_ _)m

さて、新年一発目は、今までずっと書こうと思っていて、先送りにしていたものを書こうと思います。
ESP32 および M5Stack で Arduino コーディング(プログラミング)して関数を作る時、引数が沢山ある場合はクラスや構造体を使うとメチャメチャ便利だということと、値渡し、参照渡し、ポインタ渡しのメモリの使われ方を検証したり、C/C++ コーディングスタイルと Arduino コーディングスタイルの違いや、String クラスの引数の渡し方等々、自分なりの解釈で述べてみたいと思います。

スポンサーリンク

これは、プログラミング熟練者にとっては当たり前なことかもしれませんので、分かり切っている方は読み飛ばしてください。
それに、素人の私なりの解釈が多々入っていますのでご注意ください。

実は、私が今まで作ったライブラリで、自分で作ったクラスについてはメモリの管理に気を付けていたつもりでしたが、Arduino 標準ライブラリの String クラスについては、通常の変数のように適当に扱っていて、スルーしていました。
(反省・・・)
String 型も同じクラスなので、そういえば扱いは同じだったということが、つい最近気が付いた次第です。
ということで、私が過去に作ったライブラリはメモリを無駄遣いしていたことになります。

今後、ライブラリを作る場合は以下に述べることに気を付けて作っていきたいと思います。
過去に作ったライブラリは、今更変更できないので、そのまま置いておきます。

また、Arduino コーディングについては、独特のスタイルがあります。
これは、職業プログラマの間では一般的な C/C++ コーディングスタイルとはちょっと異なります。
特に、関数で参照渡しをする場合が良い例かもしれません。
この辺のことはちゃんと考えると意外と面白いと思いますので、興味ある方はいろいろ調べてみて下さい。

因みにいつも申し上げておりますが、私は独学素人アマチュアです。
勘違いや誤りがあるかも知れませんので、何かお気づきの点がありましたらコメント投稿等でご連絡いただけると助かります。

    【目次】

  1. 使用したもの
  2. 予め Arduino core for the ESP32 をインストールしておく
  3. 関数の引数が多い場合、構造体やクラスを使うとメチャ便利!
  4. 関数の引数にクラスや構造体を使う場合、メモリ節約のために参照渡しかポインタ渡しを使うべし
  5. コーディング規約による参照渡しに const を付けなければいけない理由と、ポインタ渡しについて
  6. Arduino プログラミング開発の場合、関数への参照渡しは const を付けなくても良いのかな?
  7. Arduino の String 型もクラスなので、関数の引数にする場合は参照渡しにすべし
  8. まとめ

使用したもの

いつものとおり、Arduino core for the ESP32 で実験しました。
残念ながら、Arduino UNO ではヒープメモリのサイズは測れないので、使えないと思います。
Espressif Systems 社の ESP32 を搭載している以下のデバイスのどれでも検証できます。

(追記)
M5Stack Basicは、この記事を書いた当時より格段にバージョンアップしております。
以下のスイッチサイエンスさんの公式サイトをご参照ください。
https://www.switch-science.com/collections/%E5%85%A8%E5%95%86%E5%93%81/products/9010

※M5Stack Gray(9軸IMU搭載)現在は販売終了しております

 

ESPr Developer 32
スイッチサイエンス(Switch Science)

予め Arduino core for the ESP32 をインストールしておく

事前に Arduino IDE に Arduino core for the ESP32 をインストールしておいてください。
インストール方法は以下の記事を参照してください。

Arduino core for the ESP32 のインストール方法

関数の引数が多い場合、構造体やクラスを使うとメチャ便利!

例えば、ディスプレイの1ピクセルに座標位置やカラーの値を扱うような関数を作って、引数に入力するプログラムを考えてみます。
以下の様な感じです。

【ソースコード】 (※無保証 ※PCの場合、ダブルクリックすればコード全体を選択できます)

void setup() {
  Serial.begin(115200);
  Serial.println();

  uint16_t x0, y0, x1, y1;
  uint8_t red, green, blue;

  x0 = 10;
  y0 = 20;
  x1 = 100;
  y1 = 200;
  red = 5;
  green = 15;
  blue = 25;

  printPixelData( x0, y0, x1, y1, red, green, blue );
}

void loop() {

}

void printPixelData( uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint8_t red, uint8_t green, uint8_t blue ) {
  Serial.print( "x0 = " );
  Serial.println( x0 );
  Serial.print( "y0 = " );
  Serial.println( y0 );
  Serial.print( "x1 = " );
  Serial.println( x1 );
  Serial.print( "y1 = " );
  Serial.println( y1 );
  Serial.print( "red = " );
  Serial.println( red );
  Serial.print( "green = " );
  Serial.println( green );
  Serial.print( "blue = " );
  Serial.println( blue );
}

実行結果

パラメーターが多くて、16行目や、23行目の関数の引数もメチャメチャ長くなってしまいます。
グラフィックを扱う場合、引数が多いと見にくいし、プログラミングがとっても面倒です。

私は M5Stack のディスプレイにグラフィック表示するライブラリを自作していますが、パラメーターや引数がこれの数倍多くて、ちょっと前までは悩みどころの一つでした。
これを解決する方法があったのです。

関数の引数に構造体を使うと便利

上記の場合、構造体を使うとメッチャ便利で、使い勝手が良いです。
以下の様な感じです。

【ソースコード】 (※無保証 ※PCの場合、ダブルクリックすればコード全体を選択できます)

struct Pixel {
  uint16_t x0, y0, x1, y1;
  uint8_t red, green, blue;
};

void setup() {
  Serial.begin(115200);
  Serial.println();

  struct Pixel pix;

  pix.x0 = 10;
  pix.y0 = 20;
  pix.x1 = 100;
  pix.y1 = 200;
  pix.red = 5;
  pix.green = 15;
  pix.blue = 25;

  printPixelData( pix );
}

void loop() {
}

void printPixelData( const Pixel &p ) {
  Serial.print( "x0 = " );
  Serial.println( p.x0 );
  Serial.print( "y0 = " );
  Serial.println( p.y0 );
  Serial.print( "x1 = " );
  Serial.println( p.x1 );
  Serial.print( "y1 = " );
  Serial.println( p.y1 );
  Serial.print( "red = " );
  Serial.println( p.red );
  Serial.print( "green = " );
  Serial.println( p.green );
  Serial.print( "blue = " );
  Serial.println( p.blue );
}

実行結果

見ての通り、全く同じ結果が出ますね。

1~4行目で、Pixel という構造体宣言しています。
そして、setup関数内の 10行目で、pix という実体を生成して、12~18行でそれぞれに値を代入して初期化しています。
pix.x0 = 10;
pix.y0 = 20;
という形なので、Arduino に慣れている人にとっては明快で分かりやすい変数名です。

そうすれば、後は20行の
printPixelData( pix );
ように、pix という名を関数に入れるだけで、26~41行の自作関数 pirntPixelData に全ての値が渡せるのです。

構造体は、一回宣言してしまえばいろいろ使い回すことができるので、関数の引数が多い場合にはメチャメチャ便利です。

関数の引数にクラスも使うと便利

また、C言語の構造体が更に進化して、使い勝手が良くなった C++ のクラスも同じように関数の引数として使えます。
Arduino core for the ESP32 の場合は C++ 言語も使えるので、これを使わない手はありません。
こんな感じです。

【ソースコード】 (※無保証 ※PCの場合、ダブルクリックすればコード全体を選択できます)

class Pixel {
  public:
    uint16_t x0, y0, x1, y1;
    uint8_t red, green, blue;
};

void setup() {
  Serial.begin(115200);
  Serial.println();

  Pixel pix;

  pix.x0 = 10;
  pix.y0 = 20;
  pix.x1 = 100;
  pix.y1 = 200;
  pix.red = 5;
  pix.green = 15;
  pix.blue = 25;

  printPixelData( pix );
}

void loop() {

}

void printPixelData( const Pixel &p ) {
  Serial.print( "x0 = " );
  Serial.println( p.x0 );
  Serial.print( "y0 = " );
  Serial.println( p.y0 );
  Serial.print( "x1 = " );
  Serial.println( p.x1 );
  Serial.print( "y1 = " );
  Serial.println( p.y1 );
  Serial.print( "red = " );
  Serial.println( p.red );
  Serial.print( "green = " );
  Serial.println( p.green );
  Serial.print( "blue = " );
  Serial.println( p.blue );
}

実行結果

ほとんど構造体と同じように使えますね。
実行結果も同じです。
C++の場合は、アクセス制限を簡単にコントロールできて、とても便利です。構造体もクラスも両方ともアクセス制限が可能です。
私は、最近はもっぱらこのクラスを使っています。

2行目の public というものは、外部からの読み取り、書込み等の全アクセス可能なものです。
private にするとアクセスできなくなります(クラスの場合はデフォルトでprivate、構造体の場合はデフォルトでpublic)。
この辺の詳しいことはネット上で情報が豊富ですので、ここでは割愛させていただきます。

さて、ここで、28行目にあるような
const Pixel &p
という引数に疑問を持ちませんか?

実は、メモリ節約の面と、職業プログラマの間の一般常識的な書き方に習ったものです。
要するに、C言語、C++言語のコーディング規約的な書き方です。

実は、const を付けなくても同じ結果になりますし( const を付けるとその関数内の値は変更できなくなります。後で詳しく述べます)、また、28行目を
const Pixel &p → Pixel p
と変えても同じ結果が得られます。
以下の感じです。

【ソースコード】 (※無保証 ※PCの場合、ダブルクリックすればコード全体を選択できます)

class Pixel {
  public:
    uint16_t x0, y0, x1, y1;
    uint8_t red, green, blue;
};

void setup() {
  Serial.begin(115200);
  Serial.println();

  Pixel pix;

  pix.x0 = 10;
  pix.y0 = 20;
  pix.x1 = 100;
  pix.y1 = 200;
  pix.red = 5;
  pix.green = 15;
  pix.blue = 25;

  printPixelData( pix );
}

void loop() {

}

void printPixelData( Pixel p ) {
  Serial.print( "x0 = " );
  Serial.println( p.x0 );
  Serial.print( "y0 = " );
  Serial.println( p.y0 );
  Serial.print( "x1 = " );
  Serial.println( p.x1 );
  Serial.print( "y1 = " );
  Serial.println( p.y1 );
  Serial.print( "red = " );
  Serial.println( p.red );
  Serial.print( "green = " );
  Serial.println( p.green );
  Serial.print( "blue = " );
  Serial.println( p.blue );
}

実行結果

こんな感じで、いずれも全く結果は同じなのですが、実はメモリの使われ方で違いが生じます。
これについては以降で順番に説明していきます。

コメント

  1. Kenta IDA より:

    Arduino言語にしろC++にしろ、ポインタの場合と違って参照渡しの場合はconstつけた方がいいとかは無いと思います。
    constつける理由はもっと直接的な理由で、関数内で引数の内容を変更しないことを表すためです。
    ですので、ポインタの場合でも関数内で変更しないのであればconstつけますし、参照の場合でも関数内で変更するのであればconstつけないです。

    ポインタと参照の使い分けは、NULL (nullptr) を許容するかどうかが大きいと思います。参照は基本的にNULL にすることができません。一方、ポインタはNULL にすることができます。例えば、ポインタの場合は呼び出し時に引数にNULL を指定することによって「引数を指定しない」ことを表すことができます。
    一方、必ず指定しなければいけない引数にをポインタ型にすると、関数の内部でNULLチェックを行う必要がありますが、参照型にしておけば(無理やり入れない限りは)NULLにできませんので安全です。

    > ただし、以下のように、関数に文字列を直に渡したい場合、
    > printTestData( “hello”, “world” );

    printTestData(String(“hello”), String(“world”));
    とすれば一応渡せますが、Stringの内部バッファのヒープからの確保と解放が2回分行われるので効率が悪いです。こういう場合、可能であればconst char*を受けるオーバーロードを用意しておくと便利かと思います。

    • mgo-tec mgo-tec より:

      Kenta IDA さん

      早速コメントありがとうございます。

      なるほど!
      ポインタの NULL チェックはまったく使ったことがありませんでした。
      参照渡しとポインタ渡しの使い分けは NULL チェックが大きいとは知りませんでした。
      やっぱり独学はダメですね。
      こういうコメントで指摘されないと、自分の想像の域から脱せませんから。
      とても勉強になりました。
      手が空いたら、その辺をもう一度調べ直してみます。
      丁寧なコメント、ありがとうございました。
      m(_ _)m

      • mgo-tec mgo-tec より:

        それと、
        printTestData( “hello”, “world” ); 
        というところは、const char* の別関数を作ってオーバーロードさせるっていうところも、その手があったか!!!
        という感じで目から鱗です。
        すばらしいです。
        教えて頂き、ありがとうございます。
        m(_ _)m

  2. take4 より:

    もしよかったら、Effective C++を一読されたらいかがでしょうか。C++の設計/実装方法に関する定番のようなものが載っています。
    参照やconstを使う理由についても書かれています。

    ただ、本の記述として回りくどい書き方もあったりして、通しで読むには骨が折れます。まず目次に書かれている内容を覚えておき、自分が実装したものに照らし合わせ該当する箇所を読んで適用をしていけば、いいかもしれません。

    • mgo-tec mgo-tec より:

      take4 さん

      コメント頂き、ありがとうございます。

      ネットで調べているだけで、最近本を買っていませんでした。
      やっぱり、正しい知識は書籍の方かも知れませんね。
      私の場合は、職業プログラマのコーディングスタイルではなく、あくまで Arduino コーディングスタイルを目指しているので、その辺の違いももっとよく勉強したいですね。
      そうすると、正規のコーディングスタイルを勉強しておかないと、違いが分からないかも知れませんね。
      なかなか書籍を見る暇がありませんが、購入を検討してみたいと思います。
      情報ありがとうございました。
      m(_ _)m

  3. たらんてら より:

    ポインタと参照の違いについて。NULLの許容云々という面もありますが、一番の違いは「ポインタは動かせる、参照は動かせない」ということです。
    (文字列へのポインタを++してみるとわかりやすいでしょう)

    ポインタは仕組み上原始的で、使い方次第でとても便利ですが、一方でバグの温床にもなり易く、最悪プログラムが暴走したりします。

    参照は、ポインタの便利な機能の一部分を、安全に使うために後から考え出された仕組みです。似ているのは当然で、違いについて考えてしまうのもわかりますが、ポインタでなければできない場合以外は参照を使うべきです。

    • mgo-tec mgo-tec より:

      たらんてら さん

      記事をごらんいただき、ありがとうございます。
      只今、当ブログの不具合が生じて、メンテナンスに追われております。
      ですから、コメントが表示されたり、表示されなかったり、過去記事が消えたりと、いろいろ修正中です。
      お返事が遅くなり、申し訳ございませんでした。

      ポインタと参照の歴史は知りませんでした。
      私はネットで調べた情報しか知らないもので、まだまだ未熟でした。
      やはり、熟練者からそういう情報を頂くと、とてもよく分かりますね。
      奥が深いです。
      情報を下さってありがとうございます。
      m(_ _)m

  4. chimu2 より:

    いやぁ、コード始めたばかりって、こういう細かい葛藤ありますよねぇ。
    ただ、なぜそのように書くのかっていう理由って、あまり記述されているものってなかなか見つからないし、時間もなくて適当にサンプルのマネしちゃうので、技術力があがらないのだと思います。
    すごく参考になりましたし、こういう発信をしてもらえると初心者には特に理解の助けになると思います。頑張ってください!

    • mgo-tec mgo-tec より:

      chimu2 さん

      記事をご覧いただき、ありがとうございます。
      そう思っていただけてうれしいですね。

      この記事は2019年1月頃に書いたもので、最近はC/C++コーディングから離れていたので、久々に読み返しちゃいました。
      そうだったっけ? と我ながら思ってしまいました。
      最近はなかなか時間が無いのですが、またコーディング漬けな時間が欲しいと思う次第です。
      こういうことを疑問に思う時間は貴重ですね。

タイトルとURLをコピーしました