第4回

配列・メモリ・ポインタ・参照

C++で頻繁に登場するメモリとアドレスの基本を理解する。

配列

// 5つの整数を格納する配列
int values[5];

values[0] = 10;
values[1] = 20;

配列とループ

// values[0]~values[4]を表示する
for (int i = 0; i < 5; i++) {
    Serial.println(values[i]);
}

配列の要素をループで処理する際、添字の範囲を注意する。

メモリ

変数
 ↓
メモリ上の領域

変数には「値」だけでなく「どこに保存されているか」というアドレスがある。

アドレス演算子

int value = 10;

int* ptr = &value;

&valueはvalueのアドレス。int*は「intを指すポインタ」という型である。ptrはvalueのアドレスを保持するポインタである。

デリファレンス

*ptr = 20;

ptrが指している場所の値を変更する。この行のあとvalueは20になっている。

ポインタのインクリメント

意味
ptr++ポインタを1つ進める
*ptr++指している値を取得してからポインタを1つ進める
(*ptr)++指している値を1つ増やす

参照

int value = 10;

int& ref = value;

refはvalueの別名として扱える。ref = 20;と書けばvalueが変わる。

ポインタと参照の比較

ポインタ参照
アドレスを保持○内部的に利用
nullptr○×
再代入○基本的に×
*が必要○×

動かして確かめる

演習に入る前に、まずこのサンプルを動かして結果を見る。追加の部品は要らない。光るのはボードに載っている内蔵LEDで、それ以外はシリアルモニタの出力で確かめられる。code/examples/04-pointers/を開き、pio run -t uploadで書き込んでからpio device monitorで出力を見る(手順と、外付けのLEDなどをつなぐ場合は開発環境にまとめてある)。

code/examples/04-pointers/src/main.cpp(抜粋)
/**
 * @brief ポインタ経由で呼び出し元の変数を1増やす
 * @param[in,out] value 増やす変数へのポインタ
 */
void addOne(int* value) {
    *value = *value + 1;
}

/**
 * @brief 参照経由で呼び出し元の変数を1増やす
 * @param[in,out] value 増やす変数への参照
 */
void addOneRef(int& value) {
    value = value + 1;
}

/**
 * @brief 値をコピーして受け取り、1増やす(呼び出し元は変わらない)
 * @param value 増やす値
 */
void addOneCopy(int value) {
    value = value + 1;
}
同じファイルの setup() より
    Serial.println("--- アドレスと値 ---");
    int value = 10;
    int* ptr = &value;

    Serial.print("value    = ");
    Serial.println(value);
    Serial.print("&value   = 0x");
    Serial.println((uintptr_t)&value, HEX);
    Serial.print("ptr      = 0x");
    Serial.println((uintptr_t)ptr, HEX);  // &value と同じ
    Serial.print("*ptr     = ");
    Serial.println(*ptr);                // value と同じ

    *ptr = 20;
    Serial.print("*ptr = 20 のあと value = ");
    Serial.println(value);  // 20

    Serial.println("--- 参照 ---");
    int& ref = value;
    ref = 30;
    Serial.print("ref = 30 のあと value = ");
    Serial.println(value);  // 30

    Serial.println("--- 関数に渡す ---");
    int n = 0;

    addOneCopy(n);
    Serial.print("addOneCopy の後 : ");
    Serial.println(n);  // 0(コピーを増やしただけ)

    addOne(&n);
    Serial.print("addOne     の後 : ");
    Serial.println(n);  // 1

    addOneRef(n);
    Serial.print("addOneRef  の後 : ");
    Serial.println(n);  // 2
}

void loop() {
}
シリアルモニタの出力
--- 配列 ---
values[0] = 120
values[1] = 340
values[2] = 95
values[3] = 870
values[4] = 410
max = 870
--- アドレスと値 ---
value    = 10
&value   = 0x4085FD1C
ptr      = 0x4085FD1C
*ptr     = 10
*ptr = 20 のあと value = 20
--- 参照 ---
ref = 30 のあと value = 30
--- 関数に渡す ---
addOneCopy の後 : 0
addOne     の後 : 1
addOneRef  の後 : 2

確かめること。

  • &valueとptrが同じ値を表示している。ポインタは「どこにあるか」を持っている
  • アドレスの数値そのものは実行ごと・環境ごとに変わる。意味があるのは「2つが一致すること」だけ
  • addOneCopyではnが変わらない。コピーを1増やして捨てている
  • addOne(ポインタ)とaddOneRef(参照)はどちらもnを変える。呼び出し側に&が要るかどうかだけが違う

演習

  • 配列の最大値を求める
  • ポインタを使って関数から変数を変更する
  • 参照を使って関数から変数を変更する
void addOne(int* value) {
    *value = *value + 1;
}

void addOneRef(int& value) {
    value = value + 1;
}