関数の引数にクラスや構造体を使う場合、メモリ節約のために参照渡しかポインタ渡しを使うべし
では、関数の引数にクラスを使った場合、メモリの使われ方に注目してみたいと思います。
まずは、String型の文字列をシリアルモニタに表示させる簡単なプログラムです。
以下をご覧ください。
【ソースコード】 (※無保証 ※PCの場合、ダブルクリックすればコード全体を選択できます)
class Test { public: String str1; String str2; }; void setup() { Serial.begin(115200); Serial.println(); Test test; test.str1 = "abcdefghijklmnopqrstuvwxyz"; test.str2 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; printTestData( test ); } void loop() { Test test; test.str1 = "abcdefghijklmnopqrstuvwxyz"; test.str2 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; printTestData( test ); delay(30000); } void printTestData( Test t ) { Serial.println( t.str1 ); Serial.println( t.str2 ); }
実行結果
これは関数の引数にクラスを使って値を渡しています。
String型もクラスの一つですが、ヒープメモリを使うため、空きサイズを測るために便利なので使いました。
(String クラスについては、後で詳しく述べます。)
これ、特に何も問題無いように見えますね。
では、ヒープメモリの使われ方を見てみたいと思います。
要所要所に Arduino – ESP32 のヒープメモリ空きサイズを見るため、以下の関数、
esp_get_free_heap_size()
を置いて、シリアルモニタに表示させるプログラムに替えてみます。
以下の様な感じです。
【ソースコード】 (※無保証 ※PCの場合、ダブルクリックすればコード全体を選択できます)
class Test { public: String str1; String str2; }; void setup() { Serial.begin(115200); Serial.println(); Serial.print( "Free Heap Size(setup0) = " ); Serial.println( esp_get_free_heap_size() ); Test test; Serial.print( "Free Heap Size(setup1) = " ); Serial.println( esp_get_free_heap_size() ); test.str1 = "abcdefghijklmnopqrstuvwxyz"; test.str2 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; Serial.print( "Free Heap Size(setup2) = " ); Serial.println( esp_get_free_heap_size() ); printTestData( test ); Serial.print( "Free Heap Size(setup3) = " ); Serial.println( esp_get_free_heap_size() ); } void loop() { Serial.println("-------------------------------"); Serial.print( "Free Heap Size(main loop0) = " ); Serial.println( esp_get_free_heap_size() ); Test test; Serial.print( "Free Heap Size(main loop1) = " ); Serial.println( esp_get_free_heap_size() ); test.str1 = "abcdefghijklmnopqrstuvwxyz"; test.str2 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; Serial.print( "Free Heap Size(main loop2) = " ); Serial.println( esp_get_free_heap_size() ); printTestData( test ); Serial.print( "Free Heap Size(main loop3) = " ); Serial.println( esp_get_free_heap_size() ); delay(30000); } void printTestData( Test t ) { Serial.print( "Free Heap Size(function0) = " ); Serial.println( esp_get_free_heap_size() ); Serial.println( t.str1 ); Serial.println( t.str2 ); }
実行結果
これを見ると、ソースコードの 14行目の
Test test
を見てください。
Test クラスに、新たに test オブジェクトという実体が生成されて、メモリが 64 byte 食われます。
これはクラスや構造体を使う場合は必ずメモリが食われるので仕方ありません。
ここからヒープメモリの空きサイズが減っています。
そして、19-20行で test オブジェクトの String 型変数に文字列を代入することによって、更にメモリが 36 byte 減っています。
文字列は 26×2 = 52 文字なので、52 byte 前後かと思われますが、かなり異なりますね。
これの原因はいろいろある為、ここでは割愛します。
25行目で自作関数の printTestData に入ったところで、更に大幅に 104 byteメモリが減っています。
ここが納得いかないところだと思います。
その関数を抜けると、関数に入る前のメモリに復活して、さらにセットアップ関数を抜けてメインループに入るときに、全てのメモリが解放されて、最初のヒープメモリサイズに復活していることが分かると思います。
このような関数の引数の渡し方は、「値渡し」というものです。
では次に、最初の方で述べたように、printTestData関数の引数に ‘&’ を付けてみます。
以下のようなスケッチになります。
【ソースコード】 (※無保証 ※PCの場合、ダブルクリックすればコード全体を選択できます)
class Test { public: String str1; String str2; }; void setup() { Serial.begin(115200); Serial.println(); Serial.print( "Free Heap Size(setup0) = " ); Serial.println( esp_get_free_heap_size() ); Test test; Serial.print( "Free Heap Size(setup1) = " ); Serial.println( esp_get_free_heap_size() ); test.str1 = "abcdefghijklmnopqrstuvwxyz"; test.str2 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; Serial.print( "Free Heap Size(setup2) = " ); Serial.println( esp_get_free_heap_size() ); printTestData( test ); Serial.print( "Free Heap Size(setup3) = " ); Serial.println( esp_get_free_heap_size() ); } void loop() { Serial.println("-------------------------------"); Serial.print( "Free Heap Size(main loop0) = " ); Serial.println( esp_get_free_heap_size() ); Test test; Serial.print( "Free Heap Size(main loop1) = " ); Serial.println( esp_get_free_heap_size() ); test.str1 = "abcdefghijklmnopqrstuvwxyz"; test.str2 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; Serial.print( "Free Heap Size(main loop2) = " ); Serial.println( esp_get_free_heap_size() ); printTestData( test ); Serial.print( "Free Heap Size(main loop3) = " ); Serial.println( esp_get_free_heap_size() ); delay(30000); } void printTestData( Test &t ) { Serial.print( "Free Heap Size(function0) = " ); Serial.println( esp_get_free_heap_size() ); Serial.println( t.str1 ); Serial.println( t.str2 ); }
実行結果
どうでしょうか?
これは、「参照渡し」という方法です。
クラスのオブジェクトの中の値を格納しているメモリのアドレスだけを関数に渡して、メモリを消費している実体は渡さないというやつです。
これを見ると、printTestData関数に入る前と、入った後、関数を抜けた後で、ヒープメモリサイズは変わりありませんね。
これは、文字列がメモリされているアドレスだけを関数に渡しているので、メモリは消費されません。
ですが、これの前のプログラムのように、’&’ を付けない「値渡し」の場合、新たなクラスオブジェクトが生成されて、それに文字列を代入していくので、格納されているメモリをコピー(複製)した状態になり、メモリが余分に消費されてしまいます。
要するにネットに情報があるように、コピーコンストラクタが作成されてしまうというやつです。
クラスのパラメーターが多い場合などは、意図しない無駄なメモリ消費をしてしまうので、もったいないですね。
これは、ポインタ熟練者なら当然のことかもしれませんが、プログラミングビギナーには解らないことだと思います。
私もつい最近まで知りませんでした。
今までなんと無駄にメモリを消費していたことか・・・。
とにかく、自作関数で構造体やクラスを引数とする場合は、’&’ を付けるだけでメモリ節約になると思っておけば良いと思います。
ただ、この参照渡しを使う場合、注意点があります。
最初の方で述べた const を付けたのはいったいどういう意味だったのでしょうか?
次で述べたいと思います。
コメント
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*を受けるオーバーロードを用意しておくと便利かと思います。
Kenta IDA さん
早速コメントありがとうございます。
なるほど!
ポインタの NULL チェックはまったく使ったことがありませんでした。
参照渡しとポインタ渡しの使い分けは NULL チェックが大きいとは知りませんでした。
やっぱり独学はダメですね。
こういうコメントで指摘されないと、自分の想像の域から脱せませんから。
とても勉強になりました。
手が空いたら、その辺をもう一度調べ直してみます。
丁寧なコメント、ありがとうございました。
m(_ _)m
それと、
printTestData( “hello”, “world” );
というところは、const char* の別関数を作ってオーバーロードさせるっていうところも、その手があったか!!!
という感じで目から鱗です。
すばらしいです。
教えて頂き、ありがとうございます。
m(_ _)m
もしよかったら、Effective C++を一読されたらいかがでしょうか。C++の設計/実装方法に関する定番のようなものが載っています。
参照やconstを使う理由についても書かれています。
ただ、本の記述として回りくどい書き方もあったりして、通しで読むには骨が折れます。まず目次に書かれている内容を覚えておき、自分が実装したものに照らし合わせ該当する箇所を読んで適用をしていけば、いいかもしれません。
take4 さん
コメント頂き、ありがとうございます。
ネットで調べているだけで、最近本を買っていませんでした。
やっぱり、正しい知識は書籍の方かも知れませんね。
私の場合は、職業プログラマのコーディングスタイルではなく、あくまで Arduino コーディングスタイルを目指しているので、その辺の違いももっとよく勉強したいですね。
そうすると、正規のコーディングスタイルを勉強しておかないと、違いが分からないかも知れませんね。
なかなか書籍を見る暇がありませんが、購入を検討してみたいと思います。
情報ありがとうございました。
m(_ _)m
ポインタと参照の違いについて。NULLの許容云々という面もありますが、一番の違いは「ポインタは動かせる、参照は動かせない」ということです。
(文字列へのポインタを++してみるとわかりやすいでしょう)
ポインタは仕組み上原始的で、使い方次第でとても便利ですが、一方でバグの温床にもなり易く、最悪プログラムが暴走したりします。
参照は、ポインタの便利な機能の一部分を、安全に使うために後から考え出された仕組みです。似ているのは当然で、違いについて考えてしまうのもわかりますが、ポインタでなければできない場合以外は参照を使うべきです。
たらんてら さん
記事をごらんいただき、ありがとうございます。
只今、当ブログの不具合が生じて、メンテナンスに追われております。
ですから、コメントが表示されたり、表示されなかったり、過去記事が消えたりと、いろいろ修正中です。
お返事が遅くなり、申し訳ございませんでした。
ポインタと参照の歴史は知りませんでした。
私はネットで調べた情報しか知らないもので、まだまだ未熟でした。
やはり、熟練者からそういう情報を頂くと、とてもよく分かりますね。
奥が深いです。
情報を下さってありがとうございます。
m(_ _)m
いやぁ、コード始めたばかりって、こういう細かい葛藤ありますよねぇ。
ただ、なぜそのように書くのかっていう理由って、あまり記述されているものってなかなか見つからないし、時間もなくて適当にサンプルのマネしちゃうので、技術力があがらないのだと思います。
すごく参考になりましたし、こういう発信をしてもらえると初心者には特に理解の助けになると思います。頑張ってください!
chimu2 さん
記事をご覧いただき、ありがとうございます。
そう思っていただけてうれしいですね。
この記事は2019年1月頃に書いたもので、最近はC/C++コーディングから離れていたので、久々に読み返しちゃいました。
そうだったっけ? と我ながら思ってしまいました。
最近はなかなか時間が無いのですが、またコーディング漬けな時間が欲しいと思う次第です。
こういうことを疑問に思う時間は貴重ですね。
今まで読んだ構造体・クラス・ポインタの記事の中で一番わかった気になれました!
MILLさん
記事をご覧いただき、ありがとうございます。
しばらくこのブログ記事からは離れていたのですが、久々にこういうコメント頂くとめちゃめちゃウレシイで~す!!!
。