【C++】ポインタと参照の違い・値渡しと参照渡し・関数ポインタ・スマートポインター

この記事は約13分で読めます。

ポインタ:アドレスを格納する変数

  • ポインタ=アドレス(メモリ空間上の位置)を格納する変数
  • ポインタ変数も int 型や char 型などの基本型の変数同様に、変数宣言を行う際に、その変数用のメモリが確保され、メモリ空間上に配置される。しかし、変数宣言直後は、ポインタには不定値が格納されており、どこを指しているか分からない状態。
  • 変数名の前に & 記号をつけることで、その変数のアドレスを取得できる。
  • デリファレンス(逆参照):ポインタ変数の前に * (間接演算子)を付けることで、このポインタに格納されたアドレスのデータにアクセスできる。

詳細はこちら:https://daeudaeu.com/pointer/

ポインタ変数宣言

C++
int *p;    // ポインタ変数宣言:int型を変数を指すポインタ
// * は変数名側に寄せて付けても良いし、型名側に寄せてつけてもコンパイルは通る
int* p;  //int *p;と同じ

下記では、x'A' で初期化されているので、ptrx のアドレスを指し、*ptrptr が指す値)は 'A' になる。

C++
char x = 'A';
char *ptr;
ptr = &x;
printf("*ptr = %cn", *ptr); // *ptr = A"

下記の場合、x は初期化されていない。したがって、x に格納されている値は不定であり、*ptr の結果も未定義(undefined)。

C++
char x;
char *ptr;
ptr = &x;
printf("*ptr = %cn", *ptr);
C++
// ポインタ変数宣言時点では pNumberには具体的なメモリアドレスが割り当てられていない
int* pNumber;
// error. プログラムはポインタが指す不定の場所に10を書き込もうとする
*pNumber = 10;

ポインタ変数を宣言する際には、有効なメモリアドレスを割り当てるか、nullptrNULL で初期化することが推奨される。直接具体的なアドレス値を代入することは、特殊な状況を除いて避けるべき。

C++

// プログラムのメモリ領域に number という変数を作成し、その変数が 10 という値で初期化
int number = 10;
// number変数のメモリアドレスは、& 演算子を使って取得→これはOK
int* p = &number;
*pNumber = 41;  

// ポインタ変数宣言と初期化
//非推奨:ポインタpにリテラルアドレス 0x0000 を代入しているが、このアドレスが有効である保証はない
int* p = 0x0000;

// NULL に初期化することは有効。これはポインタがどこも指さないことを明示的に示す
int* p = nullptr;  // C++11 以降

ポインタと参照の違い

特徴ポインタ参照
定義ポインタが指し示すアドレスに格納されている値にアクセスする操作。 Windows のショートカットや Linux のシンボリックリンクと同じような機能。参照自体がデリファレンスされた値として振る舞う。
初期化初期化せずに宣言可能。nullptrに初期化可能。宣言時に必ず初期化する必要がある。
再代入異なるアドレスを指し示すように変更可能。一度初期化すると、別のオブジェクトを参照するように変更不可。
‘null’ 状態nullptrを指し示すことができる。‘null’参照は存在しない。
値にアクセスデリファレンス(逆参照)(*)を使ってアクセス。 int x = 5; int* p = &x; // pはポインタ型 int y = *p; // *を使って逆参照(でリファレンス)して値にアクセス直接アクセス可能。 int x = 100; int& r = x; // rは参照型 r = 200; // 直接値を書き換え
メモリアドレス明示的にアドレス(&)を取得して操作。アドレスを直接扱わない。
操作性アドレス算術が可能。提供される操作が限定的。
安全性ポインタは誤った使い方をすると危険。参照はより安全で、未初期化の状態がない。
使用例動的なメモリ管理、配列操作など。関数の引数や戻り値、オブジェクトのエイリアスなど。

値渡しと参照渡し

値渡し(pass by value)

  • 値渡しの場合、関数に引数として渡されるのは変数の値のコピー
  • そのため、関数内で引数の値を変更しても、それはコピーされた値に対する変更であるため、関数外の元の変数には影響しない。
  • 関数で行った変更を関数外の変数に反映させたい場合は、returnメソッドで関数から値を戻り値として返し、それを外の変数に再代入する必要がある。ただし、return で渡せるデータは1つなので、関数が呼び出し元に渡せる結果は1つのデータのみ。
C++
void increment(int value) {
    value = value + 1;
}

int x = 5;
increment(x); // この時点でのxの値はまだ5。
C++
int increment(int value) {
    return value + 1; //returnで、関数から値を戻り値として返す
}

int x = 5;
x = increment(x); // この時点でのxの値は6になる。

参照渡し(pass by reference)

  • 参照渡しの場合、関数に引数として渡されるのは変数のアドレス(または参照)。
  • 関数実行時に引数として渡されたデータの複製が作成されるのは同じだが、ポインタの場合はポインタに格納されたアドレスが複製される。したがって、ポインタ変数としては全く別のものでも、関数呼び出しにより複製されたポインタも複製元のポインタが指している場所を同様に指す。
  • このため、関数内で引数を通じて値を変更すると、その変更はメモリ上の同じアドレスに保存されている値に対して行われるため、関数外の変数に直接反映される。
  • この場合、関数外の変数を更新するために再代入する必要はない。
出典:https://daeudaeu.com/pointer/#i-10

C++で参照渡しを実現するには、2つの方法がある。

1)引数としてポインタを使用する

C++
void increment(int* value) {
    *value = *value + 1;
}

int x = 5;
increment(&x); // x=6

2)参照を使用する

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

int x = 5;
increment(x); // この時点でのxの値は6になる。

コピー時間の短縮によるプログラムの高速化

値渡しの場合

  • 引数の型によってデータサイズが決まる。たとえば、int 型の場合は通常4バイト、char 型の場合は通常1バイト。

参照渡し(ポインタ渡し)の場合

  • 関数に渡されるのはポインタであり、そのポインタのサイズはプラットフォームに依存する。32ビットアーキテクチャでは4バイト、64ビットアーキテクチャでは8バイト。

参照渡しの場合、大きなデータ構造を扱う場合や、データのサイズが実行環境に依存する場合でも、効率的に関数を呼び出すことができ、結果プログラムの実行速度を向上できる。

関数ポインタ

  • 関数を指すポインタ。関数も変数等と同様にプログラム実行時にメモリ上に展開され、メモリ上に存在することになる。
  • 関数ポインタの変数宣言。関数の戻り値と引数の型に合わせて変数宣言する:戻り値の型 (*変数名)(引数1の型, 引数2の型);
  • 関数ポインタへのアドレス格納:単純に関数ポインタへ関数名を代入すればよい。ただし、関数名には関数ポインタの変数名を使用する。
  • 関数ポインタは、コールバック関数や、実行時に関数を選択する際など、柔軟なプログラミングが必要な場合に特に有用。
C++
int functionA(int a, char b, int *c){
    /* 処理 */
}

// 関数ポインタの変数宣言
// 戻り値の型 (*変数名)(引数1の型, 引数2の型);
int (*funcPtr)(int, char, int*);

int x = 10;
char y = 'y';
int ret;

// 関数ポインタへのアドレス格納
funcPtr = functionA;

// 関数名には関数ポインタの変数名を使用する
ret = funcPtr(x, y, &x);

関数ポインタを使った場合

  • 演算子に対応する関数ポインタを operation に格納し、そのポインタを使用して選択された演算を実行する。これにより、関数の切り替えが簡単に行える。
C++
// 関数ポインタを使った場合
#include <stdio.h>

// 加算関数
int add(int a, int b) {
    return a + b;
}

// 減算関数
int subtract(int a, int b) {
    return a - b;
}

int main() {
    // 関数ポインタの宣言
    int (*operation)(int, int);

    int num1, num2;
    char operator;

    printf("Enter two numbers: ");
    scanf("%d %d", &num1, &num2);
    printf("Enter operator (+, -): ");
    scanf(" %c", &operator);

    // 選択された演算に応じて関数ポインタに対応する関数を格納
    switch (operator) {
        case '+':
            operation = add;
            break;
        case '-':
            operation = subtract;
            break;
        default:
            printf("Invalid operatorn");
            return 1;
    }

    // 関数ポインタを使用して計算を実行し、結果を表示
    int result = operation(num1, num2);
    printf("Result: %dn", result);

    return 0;
}

関数ポインタを使わない場合

  • 演算子ごとに条件分岐を使って直接関数を呼び出す必要がある
C++
#include <stdio.h>

// 加算関数と減算関数のコードは同じ

int main() {
    int num1, num2;
    char operator;

    printf("Enter two numbers: ");
    scanf("%d %d", &num1, &num2);
    printf("Enter operator (+, -): ");
    scanf(" %c", &operator);

    // 演算子ごとに関数を呼び出す
    int result;
    switch (operator) {
        case '+':
            result = add(num1, num2);
            break;
        case '-':
            result = subtract(num1, num2);
            break;
        default:
            printf("Invalid operatorn");
            return 1;
    }

    // 結果を表示
    printf("Result: %dn", result);

    return 0;
}

関数ポインタを使う場面

コールバック関数

  • 関数のアドレスを渡し、渡した先でその関数を実行してもらう場合。マルチスレッドや並列プログラミングでは、複数の処理が同時に進んでいるため、一つの処理が終わったら別の処理を実行したいという状況が発生し、その際にコールバック関数が役立つ。

addNumbers関数が通常の引数として整数を2つと、関数ポインタ(コールバック)を受け取り、特定の条件下でそのポインタを通じてコールバック関数を呼び出す。

C++
#include <iostream>

// コールバック関数の型を定義
typedef void (*Callback)(int);

// 加算を行い、結果が10以上ならコールバックを呼び出す関数
void addNumbers(int a, int b, Callback callback) {
    int result = a + b;
    std::cout << "加算結果: " << result << std::endl;
    
    if (result >= 10) {
        // コールバック関数の呼び出し
        callback(result);
    }
}

// コールバックとして使用される関数
void onResultReachedTen(int result) {
    std::cout << "結果が10以上です: " << result << std::endl;
}

int main() {
    int x = 5;
    int y = 7;

    // addNumbersに数値とコールバック関数を渡す
    addNumbers(x, y, onResultReachedTen);

    return 0;
}

メモリ管理:スマートポインター

スマートポインター

  • スマートポインターは、ポインターをより賢く使うためのツール
  • 最新のC++では、生ポインターを使用せずに、生のポインターをスマートポインターでラップすることを推奨している。
  • デストラクターが呼び出されたときにメモリを自動的に解放する。 (つまり、コードがスマート ポインターのスコープ外になった場合)。

https://learn.microsoft.com/ja-jp/cpp/cpp/smart-pointers-modern-cpp?view=msvc-170

生ポインターを使った場合:

C++
int* pNumber = new int; // 新しい整数のメモリを借りる
*pNumber = 10;          // そのメモリに10を格納する
delete pNumber;         // もうそのメモリは必要ないので返却する

スマートポインター(ここでは std::unique_ptr)を使った場合

C++
#include <memory> // スマートポインターを使うために必要なライブラリ

std::unique_ptr<int> pNumber = std::make_unique<int>(); // 新しい整数のメモリを賢く借りる
*pNumber = 10; // そのメモリに10を格納する
// ここでdeleteを呼び出す必要はない。スマートポインターが自動でメモリを返却してくれる

コメント

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