C++でのポインタの使い方についてわかりやすく詳しく解説

C++プログラミングにおいて、ポインタは非常に重要な概念です。

初心者の方にとっては理解が難しい部分もあるかもしれませんが、本記事ではわかりやすくポインタの使い方を解説します。

サンプルコードを交えながら、ポインタの基礎から応用まで詳しく説明していきます。

C言語プログラミングの基礎を理解している方であれば、この記事を読むことでC++におけるポインタの扱い方をマスターすることができます。

目次

ポインタとは何か

ポインタとは、メモリ上のアドレスを格納するための変数です。

C++では、変数に対して&演算子を用いることでその変数のアドレスを取得することができます。

また、*演算子を用いることで、ポインタが指し示すアドレスに格納されている値を参照することができます。

例えば、以下のようなコードでは、int型の変数aに対してポインタpを宣言し、pにaのアドレスを代入しています。

int a = 10;
int *p = &a;

この場合、pはaのアドレスを指し示すポインタとなります。また、*pという表記でpが指し示すアドレスに格納されている値(つまりaの値)にアクセスすることができます。

std::cout << *p << std::endl; // 出力結果:10

ポインタの使い方

ポインタは、変数のアドレスを格納するための特別な変数です。C++では、ポインタを宣言する方法がいくつかあります。

ポインタの初期化方法

ポインタの初期化方法について詳しく見ていきましょう。

ポインタ変数を宣言した後、その変数が指すメモリアドレスを設定する必要があります。

NULLで初期化する方法

最も一般的な初期化方法は、null_ptrで初期化することです

null_ptrは、何も参照していないNULLポインタであることを示す定数で、C++ではポインタの初期化でNULLではなくnull_ptrを使うことが多いです。

int* ptr = null_ptr;

上記の例では、int型のポインタ変数ptrをnull_ptrで初期化しています。

アドレス演算子(&)を使って初期化する方法

アドレス演算子(&)を使って、既存の変数のアドレスからポインタ変数を作成することもできます。

int num = 10;
int* ptr = 

上記の例では、int型の変数numに10を代入し、そのアドレスをint型のポインタ変数ptrに代入しています。これにより、ptrはnumが格納されているメモリアドレスを指すようになります。

new演算子を使って動的にメモリ領域を確保する方法

new演算子は、動的にメモリ領域を確保し、その先頭アドレスへのポインタを返します。以下はnew演算子を使用してint型配列用のメモリ領域を確保し、その先頭アドレスへのポインタpArrayに代入する例です。

int* pArray = new int[5];

上記の例では、int型配列用に5つ分(20バイト)のメモリ領域が動的に確保されています。pArrayはこの先頭アドレス(配列要素0番目)へのポインタとなります。 注意点としては、newで確保したメモリ領域は必ずdeleteで解放しなければならないことです。

ポインタの参照方法

ポインタのアドレスを参照するには、アドレス演算子&を使用します。

アドレス演算子を変数名の前に付けることで、その変数のメモリ上のアドレスを取得することができます。

例えば、以下のようなコードでは、変数numのアドレスをポインタ変数ptrに格納しています。

int num = 10;
int* ptr = # // `&num`でnumのアドレスを取得し、ポインタ変数`ptr`に格納

また、ポインタ変数自体もアドレスを持っており、そのアドレスを取得することもできます。これは、「ポインタのポインタ」と呼ばれます。

以下のようなコードでは、ポインタ変数ptr1が指す先(つまり、ptr2)のアドレスを取得しています。

int num = 10;
int* ptr1 = &num // `&num`でnumのアドレスを取得し、ポインタ変数`ptr1`に格納
int** ptr2 = &ptr1; // `&ptr1`でptr1のアドレスを取得し、ポインタ変数`ptr2`に格納

ポインタの演算

ポインタは、アドレスの数値を格納する変数です。そのため、ポインタに対して演算を行うことができます。

ポインタの加算・減算演算子

ポインタに対して加算・減算演算子を使用することで、ポインタが指すアドレスを移動させることができます。

例えば、以下のようなコードでは、p が指すアドレスから sizeof(int) バイト分だけ移動したアドレスを q に代入しています。

int a[5] = {1, 2, 3, 4, 5};
int* p = &a[2];
int* q = p + sizeof(int);

この場合、p の値は &a[2](つまり a 配列の3番目の要素のアドレス)であり、q の値は &a[3](つまり a 配列の4番目の要素のアドレス)になります。

また、減算演算子 - を使用することもできます。例えば、以下のようなコードでは、p が指すアドレスから sizeof(int) バイト分だけ戻ったアドレスを r に代入しています。

int a[5] = {1, 2, 3, 4, 5};
int* p = &a[2];
int* r = p - sizeof(int);

この場合、p の値は &a[2](つまり a 配列の3番目の要素のアドレス)であり、r の値は &a[1](つまり a 配列の2番目の要素のアドレス)になります。

ポインタ同士の引き算演算子

ポインタ同士を引くことで、それらが指すメモリ領域間隔を求めることができます。例えば以下のようなコードでは、配列内で隣り合う要素間隔を求めています。

int a[5] = {1, 2, 3, 4, 5};
int* p = &a[0];
int* q = &a[1];
std::cout << "pからqまで" << q - p << "要素離れています" << std::endl;

この場合、「pからqまで」1要素離れているため、1と表示されます。また、「qからpまで」引く場合は負数が返ってくるため注意しましょう。

ポインタの配列

ポインタの配列とは、複数のポインタを配列として扱うことです。

例えば、int型の変数を指すポインタを3つ宣言し、それらを配列に格納する場合は以下のように書きます。

int* ptrArray[3];

このように宣言することで、ptrArray[0]ptrArray[1]ptrArray[2] の3つの要素が作成されます。各要素は int 型の変数を指すポインタです。

また、ポインタの配列は二次元配列と同様に扱うこともできます。例えば以下のようなコードでは、int型の値を持つ2次元配列を作成し、その各要素に対応するポインタを格納した配列を作成しています。

int arr[2][3] = {{1, 2, 3}, {4, 5, 6}};
int* ptrArr[2];

for (int i = 0; i < 2; i++) {
    ptrArr[i] = arr[i];
}

cout << ptrArr[0][1]; // 出力結果: 2

このようにすることで、arr[i][j] ptrArr[i][j] のようにアクセスすることができます。

ポインタを使った関数

ポインタを関数の引数として渡すことができます。これにより、関数内でポインタが指し示す値を変更することができます。

例えば、以下のようなswap関数を考えてみましょう。

void swap(int* a, int* b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

この関数は、int型のポインタaとbを引数に取り、それらが指し示す値を交換します。

具体的には、temp変数にaが指し示す値を一時的に保存し、aが指し示す値をbが指し示す値で上書きします。

そして、bが指し示す値をtemp変数の値で上書きします。

この関数を使うと以下のようになります。

#include <iostream>

void swap(int* a, int* b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main(){
    
    int a = 3;
    int b = 5;
    std::cout << "a = " << a << "\tb = " << b << std::endl;

    swap(&a, &b);
    std::cout << "a = " << a << "\tb = " << b << std::endl;


   return 0;
}
a = 3	b = 5
a = 5	b = 3

このようにポインタを使って関数内で変数の値を変更することは非常に便利です。しかし、誤った使い方や扱い方によってはプログラム全体が壊れてしまう可能性もあるため注意が必要です。

ポインタの応用例

ポインタは、メモリアドレスを扱うための重要な概念です。ここでは、ポインタの応用例について説明します。

動的メモリ確保

動的メモリ確保とは、プログラム実行中に必要なだけのメモリを確保する方法です。

これは、静的メモリ確保と対比されます。静的メモリ確保では、コンパイル時に必要なだけのメモリが確保されます。

動的メモリ確保を行う場合、ポインタを使用してそのアドレスを取得します。

以下は、動的にint型変数を1つ確保し、その値を設定する例です。

int* p = new int;
*p = 10;

このようにすることで、pが指すアドレスに10が格納されます。

また、配列を動的に確保する場合は以下のように書きます。

int* p = new int[5];
for (int i = 0; i < 5; i++) {
    p[i] = i;
}

この場合、pにはint[5]の配列が動的に確保された状態になります。

ただし、動的確保したメモリは必ず解放しなければなりません。

動的確保したメモリを解放するにはdelete演算子を使用します。

delete p;

このように記述することで、動的に確保されたメモリを解放することができます。

動的に確保したメモリを解放せずにいるとメモリリークが発生するため注意が必要です。

ポインタと配列

C++では配列名自体が先頭要素へのポインタとして解釈されるため、配列とポインタは密接な関係があります。

以下は、sumという関数で配列内の全要素和を計算する例です。

int sum(int* arr, int size) {
    int total = 0;
    for (int i = 0; i < size; i++) {
        total += *(arr + i);
    }
    return total;
}

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    cout << sum(arr, sizeof(arr)/sizeof(int)) << endl; // 出力結果:15
}

sum関数では、arr自体が先頭要素へのポインタとして解釈されています。そのため、*(arr + i)といった形式でi番目の要素にアクセスすることが可能です。

以上がポインタの応用例です。

ポインタの注意点

ポインタを扱う際には、いくつか注意点があります。

1. ポインタの初期化

ポインタ変数を宣言しただけでは、そのアドレスは不定値となります。そのため、必ず初期化を行うようにしましょう。初期化しない場合、予期せぬ動作が発生する可能性があります。

2. NULLポインタの扱い

NULLポインタとは、何も指していない状態のポインタです。

NULLポインタを参照することは危険であり、プログラムがクラッシュする原因となることがあります。そのため、NULLチェックを行ってからポインタを使用するようにしましょう。

int* ptr = null_ptr;
if (ptr != null_ptr) {
    // ポインタを使用する処理
}

3. ポインタの解放

動的にメモリ確保した場合、必ず解放するようにしましょう。

解放しない場合、メモリリークが発生し、プログラムのパフォーマンス低下やクラッシュの原因となります。また、解放後に再度その領域を参照した場合も同様に危険です。

int* ptr = new int;
// メモリ確保後の処理
delete ptr; // 解放

4. 配列要素へのアクセス

配列要素へのアクセス時には、配列名+添え字でアクセスしますが、この添え字は0から始まることに注意してください。また、配列外へのアクセスは未定義動作となります。

int arr[5] = {1, 2, 3, 4, 5};
cout << arr[0] << endl; // 出力結果:1
cout << arr[4] << endl; // 出力結果:5
cout << arr[-1] << endl; // 未定義動作(エラー)
cout << arr[5] << endl; // 未定義動作(エラー)

終わりに

C++におけるポインタの基本的な使い方について解説しました。

ポインタは初心者にとっては難しい概念かもしれませんが、しっかりと理解して使いこなすことが、C++プログラミングでは不可欠です。

しっかり理解するまでは大変ですが、理解さえできればC++を8割使えるようになると言っても過言ではないので、折れないように画板って見てください。

目次