Arduino / ESP8266 ~関数間の引数・ポインタ・配列の受け渡し方 再考~

ESP8266 ( ESP-WROOM-02 )

こんばんは。

今まで先延ばしにしてきたのですが、twitterやコメント欄でいろいろな方々のご指摘を頂いて、今回ようやくこのテーマの記事をかけるようになりました。
IoT 実現で、Webと連動させるために必ず必要になってくる、ローカル関数間の引数や配列、ポインタの渡す方法について、改めて考察してみます。

ただし、前回や、前々回の記事で紹介した、Arduino IDE上でのポインタと配列の初期化や、SRAM 使用の注意点をよ~く頭に入れておかないと、理解に苦しむことになりますのでご注意ください。
特に、前回の記事では、ESPr Developer ( ESP-WROOM-02, ESP8266 ) のRAM メモリが意外と少ないことと、コンパイラメッセージでは警告が一切無く、RAMメモリにも余裕があるのにシリアルモニターではエラーになる動作には驚きましたので、そこは押さえておきたいところです。

また、私はC/C++言語やArduino 言語は全て独学ですので、間違えていることに気付かずに掲載しているかもしれません。
もし、誤っていたらコメント等でご連絡いただけると助かります。
以下の記事は予想も含んでおりますので、ご了承ください。

今回も使用するデバイスは、いつものように、Arduino と ESPr Developer ( ESP-WROOM-02, ESP8266 )です。

Arduino UNO

Arduino Uno Rev3 ATmega328 マイコンボード A000066 白
Arduino (アルドゥイーノ)
¥4,880(2024/03/28 21:45時点)

日本の電波法をクリアしているWi-Fi マイコンボード、ESP-WROOM-02 ( ESP8266 ) を Arduino IDE 上で動作させるためにお勧めのボードは ESPr Developer (スイッチサイエンス製)です。

ESP-WROOM-02開発ボード
スイッチサイエンス(Switch Science)

ESPr Developer(ピンソケット実装済)
スイッチサイエンス(Switch Science)

この使い方は以下のページを参照してください。

ESPr Developer ( ESP-WROOM-02 開発ボード )の使い方をザッと紹介

スポンサーリンク

    【目次】

  1. Arduino UNO の配列初期化の特異な振る舞い
  2. 文字列のローカル関数間の渡し方
  3. 複数の数値データをローカル関数間で受け渡す方法
  4. まとめ

Arduino UNO の配列初期化の特異な振る舞い

前々回の記事で、Arduino IDE でArduino UNO ボードを選択して、コンパイルする時、誤った配列の初期化をしていても、コンパイラが正常終了しているかのような振る舞いを見せることをお伝えしました。

それについて、コメント欄で「匿名」さんから教えていただいたことが結構面白かったので、改めてこの記事で紹介します。
これを押さえておかないと、Arduino UNO で予期せぬエラーが出た時に、原因をみつけることが困難になってしまいます。

まず、以下のスケッチの様に、明らかに配列の初期化方法が誤っているものをArduino UNOでコンパイル書き込みしてみてください。
【ソースコード】 (※無保証 ※PCの場合、ダブルクリックすればコード全体を選択できます)

// Arduino UNO でコンパイルした場合は特異
void setup() {
  delay(1000);
  Serial.begin(115200);
  while (!Serial) {
    ; // wait for serial port to connect. Needed for native USB port only
  }
  Serial.println();

  char c1[5] = "ABCDEF";
  char c2[4] = "ghijkl";
  char c3[3] = "MNOPQR";
  char c4[2] = "stuvwxyz";
  char c5[3] = "STUVWXYZ";

  Serial.println( c1 );
  Serial.println( c2 );
  Serial.println( c3 );
  Serial.println( c4 );
  Serial.println( c5 );
}

void loop() {
}

これは、見て分かる通り、ダブルクォーテーションで囲った文字列の文字数よりも明らかに少ないサイズで配列を初期化しています。
こうすると、ESP-WROOM-02 ( ESP8266 ) では確実にエラーとなってコンパイルできませんが、なぜか Arduino UNO ではコンパイルが通ってしまいます。

実は、前回の記事で述べたように、Arduino IDE の環境設定のコンパイラの警告表示を「全て」にすると、次のような警告メッセージが表示されるのを見ることができます。。

warning: initializer-string for array of chars is too long [-fpermissive]

さて、シリアルモニターの結果( Arduino IDE 1.8.1 )は特異です。

いかがでしょうか。
まったく予想外で、メチャメチャです。
14行目を以下のように変えて、

char c5[7] = "STUVWXYZ";

コンパイルしてみると、こんな風になってしまいました。

もし、ビギナーが誤った配列の初期化をしているとも知らずにコンパイルしていたら、この結果に悩みに悩んでハマってしまうでしょう。

これは要するに、Serial.print関数はNull文字終端まで文字を表示しますので、それが無い場合は、メモリに実際に記憶されている文字列の位置よりも更に先を読み込んでしまいます。
ということは、あくまで予想ですが、コンパイラがNull文字無しでメモリ内に自動的に割り当てたアドレスへ文字を格納して、c4 の2byte目にたまたま ‘\0’ があったので、そこまでスラーっと表示したと考えられます。

以上からまとめると、Arduino IDE でArduino ボードでコンパイルする時には、配列初期化は必ず(文字数+1)のサイズで初期化すべきで、そうしないと、正常にコンパイルできたと思っても予期せぬ表示やエラーに見舞われることを頭に入れてプログラミングしなければならないということです。
ま、これはArduino UNO に限ったことではないのですが・・・。

確実にエラーになってコンパイルできないようにしてもらいたいのですが、実は、前々回の記事の匿名さんのコメント欄で詳細説明がありますが、、Arduino IDE の avr-g++ コンパイラの-fpermissiveオプションを外すと、ちゃんとエラーになってコンパイルできなくなります。
C言語では、配列宣言サイズより大きい文字列でも、空き要素から順番に初期化しくという仕様のようですが、C++言語では規格上エラーになるそうです。

こういうのはユーザーが迷うので、私はデフォルトでESP8266ボードと同様にエラーになってコンパイルできないようにして欲しいですね。
ホントにこの辺はややこしいところです。
Arduino UNO コンパイルの特徴として押さえておきたいところですね。

ESP-WROOM-02 ( ESP8266 ) の場合は、呼ばれているコンパイラが異なっていて、-fpermissiveオプションも入っておらず、確実にエラーになってコンパイルできませんので心配無用です。

文字列のローカル関数間の渡し方

IoTを実現する上で、必ず必要になってくると思われるローカル関数間の文字列の受け渡しを考えてみます。
渡す方法は、return文や、配列、ポインタなどが考えられますが、それぞれ一長一短がありました。
それに、String 型文字列で渡すのか、char型文字列で渡すのかによっても使い方が異なります。
因みに、ローカル関数とは、setup関数やloop関数も含みます。

では、前回や、前々回の記事をよく踏まえた上で、ローカル関数の間で、文字列や数値を受け渡す方法を考えてみたいと思います。

String クラスを使って、一組の文字列をローカル関数間で渡す

Stringクラスは、ポインタや文字列リテラル(ダブルクォーテーションで囲った文字列)などの規則を知らなくても、気楽に文字列を扱えるとても便利なものですが、Stringクラス関数を多用するとメモリを消費してしまいます。
でも、一組の文字列をローカル関数間で受け渡しするのなら、最も簡単で確実で、しかも動的に領域を確保できます。

まず、WEBから文字列を取得する場合を想定して、String型変数にchar型文字列から一文字ずつ代入する場合のスケッチを考えていきます。
前回の記事で述べたように、ローカル関数内でのString型文字列の最大数は、

ESPr Developer ( ESP-WROOM-02, ESP8266 )では約25000文字以下、
Arduino UNO では約1644文字以下

です。
それよりも十分少ない文字数までにしておくことを念頭に置いてスケッチを組んでいく必要があります。

では、以下のスケッチをコンパイルしてみてください。
【ソースコード】 (※無保証 ※PCの場合、ダブルクリックすればコード全体を選択できます)

void setup() {
  delay(1000);
  Serial.begin(115200);
  while (!Serial) {
    ; // wait for serial port to connect. Needed for native USB port only
  }
  Serial.println();

  String str1;
  str1 = c_test_func();

  Serial.print("str1 = "); Serial.println( str1 );
}

void loop() {
}

String c_test_func(){
  char cstr[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
  String str = "";
  
  for(uint16_t i=0; i<sizeof(cstr); i++){ //変数i を int で初期化すると警告が出るので注意
    str += cstr[i];
  }
  return str;
}

ESP8266 ボードでコンパイルすると、シリアルモニターの結果はこう表示されます。

Arduino UNO の場合は1行目の文字化け以外は同じように表示されます。

19行目では、文字列リテラル(以前の記事参照)というものでchar 型配列を初期化しているので、文字配列 cstr には NULL終端文字 ‘\0’ まで入っています。
そして、char型文字列の場合は、21行目のように sizeof 関数で文字サイズを取得すると、(文字数+’\0’ )までカウントしてくれるので、forループを使う場合は便利です。

そして、最後に return で String 文字列を返すだけなので、とても分かりやすい方法です。

ただ、これは文字列一組しか返せないので、複数の文字列を返す場合はどうすれば良いでしょうか?

複数の文字列をローカル関数間で渡す方法

では、複数の文字列をローカル関数で受け渡しするにはどうすれば良いでしょうか。
return は一組しか返すことができないようなので、これは使えません。

まず、最初に抑えておきたいのは、char型ポインタを文字列リテラル(ダブルウォーテーションで囲われた文字列)で初期化して、他の関数へ渡す場合を考えます。

以下のスケッチをコンパイルしてみてください。
【ソースコード】 (※無保証 ※PCの場合、ダブルクリックすればコード全体を選択できます)

void setup() {
  delay(1000);
  Serial.begin(115200);
  while (!Serial) {
    ; // wait for serial port to connect. Needed for native USB port only
  }
  Serial.println();

  const char* cp1 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
  const char* cp2 = "I have a pen. I have a apple. Uh... Apple Pen.";

  Serial.print("cp1 = "); Serial.println( cp1 );
  Serial.print("cp2 = "); Serial.println( cp2 );
  Serial.println("****************************");
  
  c_test_func(cp1, cp2);  
}

void loop() {
}

void c_test_func(const char* c1, const char* c2){  
  Serial.print("c1 = "); Serial.println(c1);
  Serial.print("c2 = "); Serial.println(c2);
}

ESPr Developer ( ESP-WROOM-02, ESP8266 ) でコンパイルした結果のシリアルモニターは以下のようになります。

前々回の記事で述べたように、ダブルクォーテーションで囲われた文字列(文字列リテラル)は、基本的に書き換えができない領域に記憶されているので、これで初期化する場合は const を付けた方が良いです。
配列で初期化した場合は、書き換え可能な領域に記憶されます。

すると、このスケッチのように、別のローカル関数へ渡すだけであれば、ポインタで渡すことができます。

ただし、Web から送られてくる多量の文字列を処理したい場合には、まず文字列リテラルのようなconst 領域を扱うことはありませんので、これはあまり実用的ではありません。
これの使い道は、SSID やパスワードといった const 領域の文字列を渡す場合には有効です。

では、ローカル関数内で代入された2組以上の複数の文字列を別のローカル関数へ受け渡ししたい場合はどうすれば良いのでしょうか?

そこで、まず私が考えたのは、ポインタを使えば先頭アドレス渡しだけで済むので、いけるのではないかと思いました。
そこで、私はポインタをよく理解しないまま以下のような誤ったスケッチを実行してみたのです。。
【ソースコード】 (※無保証 ※PCの場合、ダブルクリックすればコード全体を選択できます)

//※これは誤ったスケッチです
void setup() {
  delay(1000);
  Serial.begin(115200);
  while (!Serial) {
    ; // wait for serial port to connect. Needed for native USB port only
  }
  Serial.println();

  char* cp1;
  char* cp2;

  char n; // ポインタの先頭アドレス設定用
  cp1 = &n;
  cp2 = &n + 53; // 先頭アドレスから53文字分(53 byte)空ける
  Serial.println((uint32_t)cp1, HEX); //ポインタの先頭アドレスを16進数で表示
  Serial.println((uint32_t)cp2, HEX); //ポインタの先頭アドレスを16進数で表示

  c_test_func(cp1, cp2);

  Serial.print("cp1 = "); Serial.println( cp1 );
  Serial.print("cp2 = "); Serial.println( cp2 );
}

void loop() {
}

void c_test_func(char* c1, char* c2){
  char cstr1[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
  char cstr2[] = "I have a pen. I have a apple. Uh... Apple Pen.";
  
  for(uint16_t i=0; i<sizeof(cstr1); i++){ //変数i を int で初期化すると警告が出るので注意
    *(c1+i) = cstr1[i];
  }  
  
  for(uint16_t ii=0; ii<sizeof(cstr2); ii++){
    *(c2+ii) = cstr2[ii];
  }
  
  Serial.print("c1 = "); Serial.println(c1);
  Serial.print("c2 = "); Serial.println(c2);
}

これを ESPr Developer ( ESP-WROOM-02, ESP8266 ) でコンパイル実行すると、前回の記事で述べたように、Arduino IDE の環境設定のコンパイラの警告表示を「全て」にして、2度コンパイルしたらエラーや警告は一切出ませんでした。
ただ、シリアルモニターには正しく文字列が表示された後、6~7秒経ってから wdt リセットを繰り返すようになりました。

因みに、これは当然、SRAMメモリも余裕があるはずなので、それに関するエラーではありません。

Arduino UNO の場合はどうなるかというと、同じくコンパイラの警告は一切出ないのですが、シリアルモニターにはこう表示されました。

ポインタの先頭アドレスは表示されますが、肝心の文字列は一切表示してくれません。

さて、これはどういうことでしょうか?

実は、私はポインタをよく理解せずに使っていたので、これの原因がなかなか分かりませんでした。
ポインタは、先頭アドレスさえ指定してやれば、あとは文字列を順番に代入していけば、配列のようにメモリされると思い込んでいました。
そして、複数の文字列を扱う場合は、次の文字列の先頭アドレスを前のアドレスから十分に空けて設定してやれば、問題ないだろうと思ってました。
こう考えた理由は、以前の記事で紹介した、外付け SRAM ではそうすればOKだったので、C/C++言語も同じだろうと思い込んでいたのです。

でも、C/C++のポインタではどうも違うようでした。

実は、答えは、渡す側の関数内で予めメモリ領域を確保すれば正常に表示されるのです。
つまり、この場合は malloc を使うことになります。
ただし、RAMのヒープ領域を使うmalloc については、前々回の記事で述べたように、極小メモリのマイコンでは極力使わない方が良い関数です。

それをよく踏まえたうえで、あえて malloc を使ってみた以下のスケッチをコンパイルしてみてください。

そういえば、free関数が抜けていました。ずっと下の方のコメント欄で、tontonさんからご指摘いただきました。
よって、このソースコードに追加しました。
tontonさん、ありがとうございました。
m(_ _)m
(2017/1/26)

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

void setup() {
  delay(1000);
  Serial.begin(115200);
  while (!Serial) {
    ; // wait for serial port to connect. Needed for native USB port only
  }
  Serial.println();

  char* cp1;
  char* cp2;

  cp1 = (char *)malloc(53 * sizeof(char));
  cp2 = (char *)malloc(53 * sizeof(char));

  Serial.println((uint32_t)cp1, HEX); //ポインタの先頭アドレスを16進数で表示
  Serial.println((uint32_t)cp2, HEX); //ポインタの先頭アドレスを16進数で表示

  c_test_func(cp1, cp2);

  Serial.print("cp1 = "); Serial.println( cp1 );
  Serial.print("cp2 = "); Serial.println( cp2 );

  free(cp1);
  free(cp2);
}

void loop() {
}

void c_test_func(char* c1, char* c2){
  char cstr1[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
  char cstr2[] = "I have a pen. I have a apple. Uh... Apple Pen.";
  
  for(uint16_t i=0; i<sizeof(cstr1); i++){ //変数i を int で初期化すると警告が出るので注意
    *(c1+i) = cstr1[i];
  }  
  
  for(uint16_t ii=0; ii<sizeof(cstr2); ii++){
    *(c2+ii) = cstr2[ii];
  }
  
  Serial.print("c1 = "); Serial.println(c1);
  Serial.print("c2 = "); Serial.println(c2);
}

すると、ESPr Developer ( ESP-WROOM-02, ESP8266 )のシリアルモニターの結果はこんな感じで正しく表示されます。

やっと期待通りの結果が出ました。

これから分かる通り、関数間で文字列をポインタで受け渡しする場合、予めmalloc で先頭アドレス設定とメモリ領域を確保しておかないとうまく動作しません。
先頭アドレス設定だけではダメなのです。

この結果の場合、cp1 と cp2 の先頭アドレスの差は

0x3FFF0214 – 0x3FFF01D4 = 0x40 = 64 ( 10進数)

となります。
つまり、53文字で指定したのに、先頭アドレスは多めの64文字後にされています。
どうやら、8文字(8byte)の倍数でアドレスが割り当てられているようです。

そして、一つ前のプログラムのように、mallocを使わずにアドレスだけ指定したものと比較すると、先頭アドレスも変わっています。
やはり、スタック領域とは異なる領域に確保されたっぽいですね。
おそらくヒープ領域がこのアドレスなのでしょう。

因みに Arduino UNO でコンパイルするとこういう結果になりました。

同じように先頭アドレスは変わって、ヒープ領域で確保されているようです。

では、次の実験として、c_text_func関数内でmalloc を使って領域を確保したら、それを受け渡せるのでしょうか?
以下のスケッチのようにしてコンパイルしてみてください。
【ソースコード】 (※無保証 ※PCの場合、ダブルクリックすればコード全体を選択できます)

//※このスケッチは動作しません。誤りです
void setup() {
  delay(1000);
  Serial.begin(115200);
  while (!Serial) {
    ; // wait for serial port to connect. Needed for native USB port only
  }
  Serial.println();

  char* cp1;
  char* cp2;

  char n;
  cp1 = &n;
  cp2 = &n + 72;

  Serial.println((uint32_t)cp1, HEX); //ポインタの先頭アドレスを16進数で表示
  Serial.println((uint32_t)cp2, HEX); //ポインタの先頭アドレスを16進数で表示

  c_test_func(cp1, cp2);

  Serial.print("cp1 = "); Serial.println( cp1 );
  Serial.print("cp2 = "); Serial.println( cp2 );
}

void loop() {
}

void c_test_func(char* c1, char* c2){
  char cstr1[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
  char cstr2[] = "I have a pen. I have a apple. Uh... Apple Pen.";

  c1 = (char *)malloc(53 * sizeof(char));
  c2 = (char *)malloc(53 * sizeof(char));
  
  for(uint16_t i=0; i<sizeof(cstr1); i++){ //変数i を int で初期化すると警告が出るので注意
    *(c1+i) = cstr1[i];
  }  
  
  for(uint16_t ii=0; ii<sizeof(cstr2); ii++){
    *(c2+ii) = cstr2[ii];
  }
  
  Serial.print("c1 = "); Serial.println(c1);
  Serial.print("c2 = "); Serial.println(c2);
}

ESPr Developer ( ESP-WROOM-02, ESP8266 ) でコンパイルするとこういう結果になりました。

結局、受け渡しができませんでした。

このスケッチに関して、ずっと下のコメント欄でtontonさんからご指摘いただきました。
ポインタのポインタを使うと動作するプログラムに変えられるそうです。
コメント欄を参照してみてください。
tontonさん、ありがとうございました。
m(_ _)m

この結果から、やはりローカル関数間での文字列のポインタ渡しは、渡す側(先方)の関数内で領域を確保しなければならないということが原則のようです。

では、前々回の記事で注意したように、malloc 関数は極力使わない方が良いとすると、Arduino や ESP-WROOM-02 ( ESP8266 ) ではどうすれば良いでしょうか?

いろいろと検討した結果、最終的には、配列で領域を確保すれば良いということに落ち着きました。
スケッチはこんな感じです。
【ソースコード】 (※無保証 ※PCの場合、ダブルクリックすればコード全体を選択できます)

void setup() {
  delay(1000);
  Serial.begin(115200);
  while (!Serial) {
    ; // wait for serial port to connect. Needed for native USB port only
  }
  Serial.println();

  char cp1[53];
  char cp2[47];
  
  c_test_func(cp1, cp2);

  Serial.print("cp1 = "); Serial.println( cp1 );
  Serial.print("cp2 = "); Serial.println( cp2 );
}

void loop() {
}

void c_test_func(char c1[], char c2[]){
  char cstr1[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
  char cstr2[] = "I have a pen. I have a apple. Uh... Apple Pen.";
  
  for(uint16_t i=0; i<sizeof(cstr1); i++){ //変数i を int で初期化すると警告が出るので注意
    c1[i] = cstr1[i];
  }  
  
  for(uint16_t ii=0; ii<sizeof(cstr2); ii++){
    c2[ii] = cstr2[ii];
  }
  
  Serial.print("c1 = "); Serial.println(c1);
  Serial.print("c2 = "); Serial.println(c2);
}

シリアルモニターの結果はこんな感じです。

先ほどと同様に、期待通りの結果が得られました。

わざわざ小難しいポインタなんか使わず、配列で渡した方が確実で、分かりやすいですね。
よって、結局は Arduino のコンセプト(?)どおり、ポインタを極力使わないことに落ち着いてしまったわけです。

以上から、関数間で複数の文字列を渡す場合には、渡す側で予め配列宣言とメモリを確保して、配列で渡した方がよいということです。
これで私的には一件落着です。

ローカル関数内での動的メモリ割り当てを考える

では、極小メモリのマイコンで IoT を実現したい場合、Webから送られてくる多量の文字列を処理したいとすると、文字サイズは分からないことが殆どです。
そうすると、ローカル関数内で動的に配列の文字数を変化させたいと考えると思います。

ローカル関数内だけで配列のメモリを完結させて、関数を抜けるとそのメモリを破棄して良いのであれば、動的メモリの割り当ては可能です。
例えば String 文字列を使って、受信した文字をヌル終端文字 ‘\0’ が来るまで追加代入していけばどんどんメモリが増えます。
ただし、この場合の注意点は前回の記事でもお伝えしたように、

ESPr Developer ( ESP-WROOM-02, ESP8266 ) は約25000 文字以下
Arduino UNO は約 1600 文字以下

です。
それでも、ローカル関数内では余裕を持たないと、プログラムが大きくなった場合には当然メモリも少なくなります。
それをよく踏まえた上で、ローカル関数内で完結するのであれば、動的メモリ割り当てもアリだと思います。
(ただ、malloc は使わない方が良いです)

取得した文字列を他の関数へ渡す場合には、上で述べたように、先方の関数側で予め領域を確保することが必要になってきます。

複数の数値データをローカル関数間で受け渡す方法

前の項では、文字列についてでしたが、数値データの場合はポインタを使った方が断然やりやすい場合があります。
特に、以下のように2組以上の文字列と文字の長さを返す場合は有効です。

以下のスケッチでは、文字列中の改行コード ‘\n’ まで文字列を取得し、その文字列の長さを2組返す場合です。
【ソースコード】 (※無保証 ※PCの場合、ダブルクリックすればコード全体を選択できます)

void setup() {
  delay(1000);
  Serial.begin(115200);
  while (!Serial) {
    ; // wait for serial port to connect. Needed for native USB port only
  }
  Serial.println();

  char cp1[60];
  char cp2[60];
  uint16_t Len1 = 0;
  uint16_t Len2 = 0;
  
  c_test_func(cp1, cp2, &Len1, &Len2);

  Serial.println("*************************");
  Serial.print("cp1 = "); Serial.println( cp1 );
  Serial.print("Len1 = "); Serial.println( Len1 );
  Serial.print("cp2 = "); Serial.println( cp2 );
  Serial.print("Len2 = "); Serial.println( Len2 );
}

void loop() {
}

void c_test_func(char c1[], char c2[], uint16_t* len1, uint16_t* len2){
  char cstr1[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefg\nhijklmnopqrstuvwxyz";
  char cstr2[] = "I have a pen. I have a apple.\n Uh... Apple Pen.";
  uint16_t i;
  
  for(i=0; i<sizeof(cstr1); i++){ //変数i を int で初期化すると警告が出るので注意
    c1[i] = cstr1[i];
    if( c1[i] == '\n' ) break;
  }
  c1[i+1] = '\0';
  *len1 = strlen(c1)+1; //この場合、sizeof(c1) では正しく計算されない。
  
  for(i=0; i<sizeof(cstr2); i++){
    c2[i] = cstr2[i];
    if( c2[i] == '\n' ) break;
  }
  c2[i+1] = '\0';
  *len2 = strlen(c2)+1; //sizeof(c1) は使えない。文字列リテラルの初期化ではなく、代入の為。
  
  Serial.print("c1 = "); Serial.println(c1);
  Serial.print("len1 = "); Serial.println( *len1 );
  Serial.print("c2 = "); Serial.println(c2);
  Serial.print("len2 = "); Serial.println( *len2 );
}

ESPr Developer ( ESP-WROOM-02, ESP8266 )でコンパイルした結果のシリアルモニターの表示はこんな感じです。

しっかりローカル関数間で2組の文字列と文字の長さが渡されていますね。
改行コードもちゃんと反映されています。
代入された数値データがローカル関数を抜けても、ちゃんと引き継がれています。
これができれば、グローバル変数を使わないで済むことが多くなります。

数値データの場合は、宣言した変数に ‘&’ を付ければ、その変数のメモリ上のアドレスを表します。
文字列のように領域を確保せずとも、uint16_t 型で宣言すれば、16bit = 2byte という領域があらかじめ確保されているということになるので、問題ありません。

ただ、受け取った関数側で数値を代入する場合には、36行目のように

*len1 = strlen(c1)+1;

アスタリスクを変数名の前に付けることを忘れないようにしてください。

uint16_t Len1;
と宣言したら、そのRAMのアドレスは
&Len1
となり、実際のデータは
Len1
ですね。

uint16_t* len1;
とポインタ宣言したら、そのRAMのアドレスは
len1
となり、実際のデータは
*len1
ということです。
ビギナーの方にはちょっとややこしいと思います。
この辺のことはネットで検索すれば、沢山参考になるサイトがありますね。

あと、このスケッチで十分注意していただきたいのは、35行と42行にあるように、最後に必ずNull文字 ‘\0’ を代入することです。
そして、36行と43行にあるように、Null文字までを含めた文字数を実際の文字列の長さとすることです。
最初に述べたように、特にArduino UNO では、Serial.print でシリアルモニタに表示する場合、文字列に ’\0’ が無いと全く予想外の表示になったりする為です。
これは十分注意したいところですね。

まとめ

以上、ローカル関数間のデータ受け渡しについて今回は考えてみました。

要するに、配列やポインタを受け渡す場合は、まず渡す側でメモリの領域を確保してからでないと受け渡しできないということです。

もし、誤っていることがありましたら、コメント等でご連絡いただけると助かります。

しかし、この結論に達するまで、とてつもなく長い時間を費やしてしまいました。
プログラミングに関してはまだまだ勉強不足なことが多いことを痛感しました。

そういえば、外付けSRAM の記事を書いたときに、これは「ポインタと同じ動作だ」と勘違いしたところから始まりました。
それを切っ掛けに、twitter や コメント投稿欄で様々な方々からご指摘やアドバイスを頂戴しました。
あまりの不勉強さにヘコんだこともありましたが、逆に前以上に理解が深まりました。
これでプログラミングのエラーに対して、かなりの免疫がついたと思います。

この勘違いが、結果的に私にはとても良い方向に向かうことになりました。
ご指摘やアドバイスを頂戴したみなさま、改めてお礼いたします。
ありがとうございました。
m(_ _)m

これで、ようやく次の電子工作に移れそうです。

ではまた・・・。

Amazon.co.jp 当ブログのおすすめ

スイッチサイエンス ESPr Developer 32 Type-C SSCI-063647
スイッチサイエンス
¥2,420(2024/03/29 13:47時点)
ZEROPLUS ロジックアナライザ LAP-C(16032)
ZEROPLUS
¥22,504(2024/03/28 20:55時点)
Excelでわかるディープラーニング超入門
技術評論社
¥1,700(2024/03/29 10:12時点)

コメント

  1. tonton より:

    はじめまして…とはいっても、twitter上では
    ESP-WROOM-02を使ったラジコンカーについて
    何度か「いいね」をいただいたことがあります。
    どうもありがとうございます。

    ESP-WROOM-02の可能性をいろいろ示してくれるサイトとして、
    以前よりちょくちょく拝見しておりました。
    頻繁な更新、お疲れ様です。

    さて本文「誤り」とされているプログラムですが、
    実に惜しいところまで行っておられます。
    この場合、ポインタそのものを引数により受け取るわけですから
    「ポインタのポインタ」というものを受け渡しします。

    私が直すとすれば、以下のようになるでしょうか。

    void setup() {
      delay(1000);
      Serial.begin(115200);
      while (!Serial) {
        ; // wait for serial port to connect. Needed for native USB port only
      }
      Serial.println();
     
      char* cp1 = 0;
      char* cp2 = 0;
      
      c_test_func(&cp1, &cp2);
     
      Serial.print("cp1 = "); Serial.println( cp1 );
      Serial.print("cp2 = "); Serial.println( cp2 );
      
      free(cp1);
      free(cp2);
    }
     
    void loop() {
    }
     
    void c_test_func(char** c1, char** c2){
      char cstr1[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
      char cstr2[] = "I have a pen. I have a apple. Uh... Apple Pen.";
      
      uint16_t len1 = strlen(cstr1);
      uint16_t len2 = strlen(cstr2);
     
      *c1 = (char *)malloc((len1 + 1) * sizeof(char));
      *c2 = (char *)malloc((len2 + 1) * sizeof(char));
       
      for(uint16_t i=0; i<len1; i++){ //変数i を int で初期化すると警告が出るので注意
        *(*c1+i) = cstr1[i];
      }
      *(*c1+len1) = '\0';
       
      for(uint16_t ii=0; ii<len2; ii++){
        *(*c2+ii) = cstr2[ii];
      }
      *(*c2+len2) = '\0';
       
      Serial.print("c1 = "); Serial.println(*c1);
      Serial.print("c2 = "); Serial.println(*c2);
    }

    なおsetup()の最後にfree()をやっておかないと、
    どこからも参照できない領域がヒープ上に残ってしまいます。
    これが「メモリリーク」と呼ばれるものです。
    このメモリ管理が存外面倒なので、それを自動でやってくれる
    String等のクラスが重宝されるというわけです。

    • mgo-tec mgo-tec より:

      tontonさん

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

      なるほど!! スゴイですね。
      ちゃんとエラー皆無で、シリアルモニターに表示されました。
      そういえば、freeを入れるのをすっかり忘れてました。
      malloc は全く使わないので、ちょっと勉強しただけの小手先だけで使ってみただけでした。
      やっぱり、熟練者からアドバイスいただくと、とても勉強になります。

      ポインタのポインタは、自分はArduino やESP-WROOM-02ではどうもうまくいかなかったので、殆ど捨てていました。
      なるほど、こうやるとちゃんと動作するんですね。
      やはりまだまだ勉強不足です。

      確かに、これはメモリ管理が難しいですね。
      やはり結局は、Arduinoの文字列の受け渡しは配列かStringクラスということですね。
      とても勉強になりました。感謝感謝です。
      ありがとうございました。
      m(_ _)m

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