C++の基礎 - ポインタ
概要
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;
}