C++の基礎 - ポインタ

提供:MochiuWiki - SUSE, Electronic Circuit, PCB
ナビゲーションに移動 検索に移動

概要

C++において、ポインタはメモリアドレスを格納する変数である。

ポインタを使用することにより、動的なメモリ確保やデータの効率的な操作が可能になるが、
適切に扱わなければメモリリークやダングリングポインタ等の問題を引き起こす危険性がある。

ポインタは、C++の基本的かつ重要な機能であり、以下に示すような場面で使用される。

  • 動的なメモリ確保
  • 関数への参照渡し
  • 配列やデータ構造の操作
  • ポリモーフィズムの実現



ポインタとは

ポインタとは、メモリアドレスを格納する変数である。
通常の変数がデータそのものを格納するのに対し、ポインタはデータが格納されているメモリ上の場所 (アドレス) を格納する。

ポインタは、以下に示すような特徴を持っている。

  • メモリアドレスを格納する。
  • 間接参照演算子(*)を使って、ポインタが指すメモリの値にアクセスできる。
  • アドレス演算子(&)を使って、変数のアドレスを取得できる。
  • 動的にメモリを確保・解放することができる。
  • nullptrを代入することで、何も指していない状態を表現できる。


 #include<iostream>
 
 int main()
 {
    // 通常の変数の宣言
    int value = 10;
 
    // ポインタの宣言と初期化
    // valueのアドレスをptrに格納する
    int* ptr = &value;
 
    // ポインタが指す値を表示
    std::cout << "valueの値: " << value << std::endl;        // 10が表示される
    std::cout << "valueのアドレス: " << &value << std::endl; // メモリアドレスが表示される
    std::cout << "ptrの値(アドレス): " << ptr << std::endl;  // valueと同じアドレスが表示される
    std::cout << "ptrが指す値: " << *ptr << std::endl;       // 10が表示される
 
    // ポインタを通じて値を変更
    // ptrが指すメモリの値を20に変更する
    *ptr = 20;
    std::cout << "変更後のvalueの値: " << value << std::endl; // 20が表示される
 
    // nullptrの使用
    // 何も指していないポインタを宣言
    int* ptr2 = nullptr;
 
    return 0;
 }



ポインタの使い方

 // ポインタの宣言には、型名の後にアスタリスク(*)を付ける
 int* ptr1;     // int型へのポインタ
 double* ptr2;  // double型へのポインタ
 char* ptr3;    // char型へのポインタ
 
 // ポインタの初期化
 int value = 100;
 int* ptr = &value;  // アドレス演算子(&)で変数のアドレスを取得
 
 // nullptrで初期化することもできる
 // nullptrは何も指していない状態を表す
 int* ptr4 = nullptr;


 // 間接参照演算子(*)を使用して、ポインタが指す値にアクセスできる
 int value = 10;
 int* ptr = &value;
 
 // ポインタを通じて値を読み取る
 // *ptrは、ptrが指すメモリの値を表す
 int readValue = *ptr;  // readValueは10
 
 // ポインタを通じて値を変更する
 // *ptrに代入すると、ptrが指すメモリの値が変更される
 *ptr = 20;  // valueの値が20になる


 // 動的メモリ確保には、new演算子を使用する
 // メモリの解放には、delete演算子を使用する
 
 // int型のメモリをヒープ領域に動的に確保
 // new int(10)は、10で初期化されたint型のメモリを確保し、そのアドレスを返す
 int* ptr = new int(10);
 
 // ポインタを使用
 std::cout << *ptr << std::endl;
 
 // メモリを解放
 // 動的に確保したメモリは、必ずdeleteで解放する必要がある
 delete ptr;
 
 // 解放後はnullptrを代入するのが安全
 // これにより、ダングリングポインタを防げる
 ptr = nullptr;


 // 配列の動的確保には、new[]演算子を使用する
 // 配列の解放には、delete[]演算子を使用する
 
 // int型配列の要素数10をヒープ領域に動的に確保
 int* array = new int[10];
 
 // 配列に値を代入
 // ポインタでも添字演算子[]を使用できる
 for(int i = 0; i < 10; i++)
 {
    array[i] = i * 2;
 }
 
 // 配列の値を表示
 for(int i = 0; i < 10; i++)
 {
    std::cout << array[i] << " ";
 }
 std::cout << std::endl;
 
 // 配列のメモリを解放(delete[]を使用)
 // new[]で確保したメモリは、必ずdelete[]で解放する
 delete[] array;
 array = nullptr;


 // クラスのメンバにアクセスするには、アロー演算子(->)を使用する
 class MyClass
 {
    public:
       int value;
       void print() { std::cout << value << std::endl; }
 };
 
 // クラスのインスタンスを動的に確保
 MyClass* ptr = new MyClass();
 
 // アロー演算子でメンバにアクセス
 // ptr->valueは、(*ptr).valueと同じ意味
 ptr->value = 100;
 ptr->print();
 
 // (*ptr).valueのように書くこともできるが、->の方が簡潔で読みやすい
 (*ptr).value = 200;
 
 // メモリを解放
 delete ptr;



関数の引数

関数に大きなデータを渡す時や、関数内で値を変更したい時にポインタを使用する。
値渡しではデータのコピーが発生するが、ポインタ渡しではアドレスのみを渡すため効率的である。

 // 値を変更する関数
 // ポインタを引数に取ることで、呼び出し元の変数を直接変更できる
 void increment(int* ptr)
 {
    (*ptr)++;  // ポインタが指す値をインクリメント
 }
 
 int main()
 {
    int value = 10;
    // valueのアドレスを渡す
    // 関数内でvalueの値が変更される
    increment(&value);
    std::cout << value << std::endl;  // 11が表示される
 
    return 0;
 }



動的メモリ確保

実行時にサイズが決まる配列や、大きなデータ構造を扱う場合にポインタを使用する。
スタック領域は限られているため、大きなデータはヒープ領域に動的に確保する必要がある。

 int main()
 {
    int size;
    std::cout << "配列のサイズを入力: ";
    std::cin >> size;
 
    // 実行時にサイズが決まる配列を動的に確保
    // スタック配列と異なり、実行時に決定したサイズで配列を作成できる
    int* array = new int[size];
 
    // 配列を使用
    for(int i = 0; i < size; i++)
    {
       array[i] = i;
    }
 
    // メモリを解放
    // 動的に確保したメモリは必ず解放する
    delete[] array;
 
    return 0;
 }


ポリモーフィズム

基底クラスのポインタを使って、派生クラスのオブジェクトを扱う場合にポインタを使用する。
これにより、実行時に適切な関数が呼び出される動的な多態性が実現できる。

 class Base
 {
    public:
       virtual void print() { std::cout << "Base" << std::endl; }
       // 仮想デストラクタを定義することで、適切にメモリが解放される
       virtual ~Base() {}
 };
 
 class Derived : public Base
 {
    public:
       // 基底クラスの仮想関数をオーバーライド
       void print() override { std::cout << "Derived" << std::endl; }
 };
 
 int main()
 {
    // 基底クラスのポインタで派生クラスのオブジェクトを指す
    Base* ptr = new Derived();
    // 実行時に派生クラスのprint関数が呼び出される
    ptr->print();  // "Derived"と表示される
    delete ptr;
 
    return 0;
 }



ポインタの注意点

メモリリーク

動的に確保したメモリを解放しないと、メモリリークが発生する。
プログラムが終了するまでメモリが占有され続け、システムのメモリが枯渇する可能性がある。

 // メモリリークの例
 void memoryLeak()
 {
    int* ptr = new int(10);
    // deleteを忘れている!
    // 関数終了時にptrは消えるが、確保したメモリは残り続ける
 }
 
 // 正しい使い方
 void noMemoryLeak()
 {
    int* ptr = new int(10);
    // 必ずdeleteする
    // これによりメモリが適切に解放される
    delete ptr;
 }


ダングリングポインタ

解放済みのメモリを指すポインタをダングリングポインタと呼ぶ。
ダングリングポインタを使用すると、未定義動作を引き起こす。

 int* ptr = new int(10);
 delete ptr;
 // この時点でptrはダングリングポインタ
 // ptrはまだアドレスを保持しているが、そのメモリは解放済み
 
 // *ptr = 20;  // 未定義動作! 絶対にやってはいけない
 
 // 解放後はnullptrを代入する
 // これによりダングリングポインタを防げる
 ptr = nullptr;
 
 // nullptrチェックで安全に使用できる
 if(ptr != nullptr)
 {
    *ptr = 20;
 }


二重解放

同じメモリを2回以上解放すると、プログラムがクラッシュする可能性がある。

 int* ptr = new int(10);
 delete ptr;
 // delete ptr;  // 二重解放! プログラムがクラッシュする可能性
 
 // 解放後にnullptrを代入すれば、二重解放を防げる
 ptr = nullptr;
 // nullptrのdeleteは安全(何もしない)
 delete ptr;


nullポインタの参照

nullptrや未初期化のポインタを参照すると、プログラムがクラッシュする。

 int* ptr = nullptr;
 // *ptr = 10;  // クラッシュする!
 
 // 使用前にnullptrチェックを行う
 // これにより安全にポインタを使用できる
 if(ptr != nullptr)
 {
    *ptr = 10;
 }
 
 // または、ポインタを必ず有効な値で初期化する
 int value = 0;
 int* ptr2 = &value;  // 有効なアドレスで初期化
 *ptr2 = 10;  // 安全


配列のdelete

new[]で確保したメモリは、delete[]で解放する必要がある。
deleteとdelete[]を混同すると、未定義動作を引き起こす。

 // 単一のオブジェクト
 // newで確保したメモリはdeleteで解放
 int* ptr1 = new int(10);
 delete ptr1;  // OK
 
 // 配列
 // new[]で確保したメモリはdelete[]で解放
 int* ptr2 = new int[10];
 delete[] ptr2;  // OK
 
 // 間違った例
 // int* ptr3 = new int[10];
 // delete ptr3;  // 未定義動作! delete[]を使う必要がある



ポインタと配列の違い

ポインタと配列は密接に関連しているが、重要な違いがいくつか存在する。
これらの違いを理解することで、ポインタと配列を適切に使い分けることができる。

宣言と初期化

配列は宣言時にサイズが固定され、連続したメモリ領域が確保される。
ポインタは単にアドレスを格納する変数であり、初期化時にメモリを指すように設定する。

 // 配列の宣言
 // コンパイル時にサイズが決まり、スタック領域に確保される
 int array[10];
 
 // ポインタの宣言
 // ポインタ変数自体はスタックに確保されるが、何も指していない
 int* ptr;
 
 // ポインタを使った動的配列
 // 実行時にサイズを決定でき、ヒープ領域に確保される
 ptr = new int[10];


メモリの配置

配列名は、その配列の先頭要素のアドレスを表す定数である。
ポインタは変数であり、別のアドレスを代入することができる。

 int array[10];
 int* ptr = array;  // 配列の先頭アドレスをポインタに代入
 
 // 配列名は定数なので、別のアドレスを代入できない
 // array = ptr;  // エラー! 配列名は定数
 
 // ポインタは変数なので、別のアドレスを代入できる
 int another[10];
 ptr = another;  // OK


sizeof演算子の結果

sizeof演算子を使った時、配列とポインタでは異なる結果が返される。
配列に対してsizeofを使うと、配列全体のサイズが返される。
ポインタに対してsizeofを使うと、ポインタ変数自体のサイズが返される。

 int array[10];
 int* ptr = array;
 
 // 配列のサイズ
 // int型のサイズ×要素数が返される
 std::cout << sizeof(array) << std::endl;  // 40バイト (int型が4バイトの場合)
 
 // ポインタのサイズ
 // ポインタ変数自体のサイズが返される
 std::cout << sizeof(ptr) << std::endl;    // 8バイト (64ビットシステムの場合)
 
 // 配列の要素数を計算
 int arraySize = sizeof(array) / sizeof(array[0]);
 std::cout << arraySize << std::endl;  // 10


ポインタ演算

配列名もポインタも、ポインタ演算を使用できる。
ただし、配列名は定数なので、インクリメントやデクリメントはできない。

 int array[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
 int* ptr = array;
 
 // 配列名を使ったアクセス
 std::cout << array[0] << std::endl;      // 0
 std::cout << *(array + 2) << std::endl;  // 2
 
 // ポインタを使用したアクセス
 std::cout << *ptr << std::endl;          // 0
 std::cout << *(ptr + 2) << std::endl;    // 2
 
 // ポインタはインクリメントできる
 ptr++;  // OK  次の要素を指すようになる
 std::cout << *ptr << std::endl;  // 1
 
 // 配列名はインクリメントできない
 // array++;  // エラー! 配列名は定数


メモリの解放

スタックに確保された配列は、スコープを抜けると自動的に解放される。
ポインタで動的に確保したメモリは、明示的に delete または delete[] で解放する必要がある。

 void stackArray()
 {
    int array[10];
    // arrayを使用
    // 関数終了時に自動的に解放される
 }
 
 void heapArray()
 {
    int* ptr = new int[10];
    // ptrを使用
    
    // 明示的に解放する必要がある
    delete[] ptr;
    // 解放を忘れるとメモリリークが発生する
 }


関数の引数としての配列とポインタ

関数の引数として配列を渡すと、実際にはポインタとして渡される。
このため、関数内で配列のサイズを知ることはできない。

 // 配列を引数に取る関数
 // 実際にはポインタとして渡される
 void printArray(int arr[], int size)
 {
    // sizeof(arr)はポインタのサイズを返すので、要素数は計算できない
    // そのため、サイズを別の引数で渡す必要がある
    for(int i = 0; i < size; i++)
    {
       std::cout << arr[i] << " ";
    }
    std::cout << std::endl;
 }
 
 int main()
 {
    int array[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    
    // 配列と、そのサイズを渡す
    printArray(array, 10);
    
    return 0;
 }


多次元配列

配列とポインタでは、多次元配列の扱い方が異なる。

 // 2次元配列の宣言
 int array2D[3][4];
 
 // ポインタを使った2次元配列
 // まず、ポインタの配列を確保
 int** ptr2D = new int*[3];
 // 次に、各行に配列を確保
 for(int i = 0; i < 3; i++)
 {
    ptr2D[i] = new int[4];
 }
 
 // 値の代入はどちらも同じ
 array2D[1][2] = 10;
 ptr2D[1][2] = 10;
 
 // ポインタで確保した2次元配列の解放
 // 各行を解放してから、ポインタの配列を解放
 for(int i = 0; i < 3; i++)
 {
    delete[] ptr2D[i];
 }
 delete[] ptr2D;



配列との比較

静的配列とポインタによる動的配列には、それぞれ利点がある。

静的配列を使用する方法は、スタック領域に確保されるため、高速にアクセスできる。
また、メモリの解放を意識する必要がない。
ただし、コンパイル時にサイズが決まっている必要がある。

ポインタを使用する方法は、実行時にサイズを決定できる。
また、大きなデータをヒープ領域に確保できる。
ただし、メモリの解放を明示的に行う必要がある。

静的配列は、サイズが固定で小さいデータに適している。
ポインタによる動的配列は、実行時にサイズが決まる場合や、大きなデータを扱う場合に適している。

ただし、現代のC++では、std::vector や std::array等のコンテナクラスの使用が推奨される。
これらのクラスは、メモリ管理を自動化し、より安全で使いやすいインターフェースを提供する。

例 : 動的配列の使用

 #include <iostream>
 
 int main()
 {
    int size;
    std::cout << "要素数を入力してください: ";
    std::cin >> size;
 
    // 動的に配列を確保
    // ユーザが入力したサイズで配列を作成できる
    int* array = new int[size];
 
    // 配列に値を代入
    for(int i = 0; i < size; i++)
    {
       array[i] = (i + 1) * 10;
    }
 
    // 配列の内容を表示
    std::cout << "配列の内容: ";
    for(int i = 0; i < size; i++)
    {
       std::cout << array[i] << " ";
    }
    std::cout << std::endl;
 
    // 配列の平均値を計算
    double sum = 0;
    for(int i = 0; i < size; i++)
    {
       sum += array[i];
    }
    double average = sum / size;
    std::cout << "平均値: " << average << std::endl;
 
    // メモリを解放
    // 動的に確保したメモリは必ず解放する
    delete[] array;
    array = nullptr;
 
    return 0;
 }