はじめに、C言語のポインタという概念について簡単に説明します。
ポインタとは、変数や配列などのメモリ上のアドレスを指し示すためのものであり、C言語では非常に重要な概念です。
ポインタを理解することで、より高度なプログラムを作成することが可能になります。本記事では、初心者でもわかるようにポインタの基礎から解説していきます。
ポインタとは?
ポインタとは、メモリ上のアドレスを格納するための変数です。
C言語では、変数に対して「&」演算子を用いることでその変数が格納されているメモリ上のアドレスを取得することができます。
そして、そのアドレスを別の変数に代入することで、その変数も同じ場所にあるデータにアクセスすることが可能になります。
ポインタは初心者には難しい概念かもしれませんが、理解すればプログラム作成時やデバッグ時などで非常に役立ちます。
ポインタの宣言方法
ポインタは、変数のアドレスを格納するための特別な変数です。C言語では、ポインタを宣言する際には「*」(アスタリスク)を使用します。
例えば、int型の変数numがある場合、そのアドレスを格納するためには以下のように宣言します。
int *p_num;
このように、「*」を使ってポインタ型であることを示し、次に変数名を指定します。
また、ポインタ変数であることがわかるように、一般的にポインタ変数名は先頭に「p_」や「ptr_」などの接頭辞をつけて差別化することが多いです。
また、複数のポインタ変数を同時宣言することも可能です。例えば以下のような形式で記述します。
int *p_num1, *p_num2;
この場合、「*」が各々のポインタ変数名前に付与されています。
アドレス演算子(&)について
アドレス演算子(&)は、変数のメモリ上でのアドレスを取得するために使用されます。
C言語では、すべての変数がメモリ上に割り当てられるため、その変数が格納されている場所を知ることが重要です。
例えば、int型の変数xを宣言した場合、&演算子を使ってxのアドレスを取得することができます。
int x = 10;
printf("x の値: %d\n", x);
printf("x のアドレス: %p\n", &x);
このコードでは、「%p」フォーマット指定子を使用して、ポインタ値(つまりアドレス)を表示します。出力結果は次のようになります。
x の値: 10
x のアドレス: 0xffe4a12c
このようにして、&演算子は特定の変数やオブジェクトが格納されているメモリ位置(つまり「アドレス」)を見つけるために使用されます。
間接参照演算子(*)について
間接参照演算子(*)は、ポインタが指し示すメモリ上の値を取得するために使用されます。つまり、ポインタ変数が指しているアドレスに格納されているデータを取得することができます。
以下は、ポインタ変数pがint型の変数xのアドレスを指している場合の例です。
int x = 10;
int *p = &x; // pはxのアドレスを指す
printf("%d\n", *p); // pが指すアドレス上にある値(10)を出力する
このように、*演算子を使ってポインタ変数から実際の値を取得することができます。また、*演算子は左結合性であり、優先順位も高くなっています。そのため、複雑な式でも適切な括弧付けさえ行えば正しく評価されます。
int x = 10, y = 20;
int *p1 = &x, *p2 = &y;
// 括弧無しバージョン
printf("%d\n", *p1 + 2 * *p2); // 出力: 50
// 括弧有りバージョン
printf("%d\n", (*p1 + 2) * (*p2)); // 出力: 240
ポインタを使った値の代入方法
ポインタを使った値の代入方法は、以下のようになります。
まず、変数を宣言し、その変数に対するポインタを作成します。例えば、int型の変数numがある場合、そのポインタを作成するには以下のように記述します。
int num;
int *p_num;
次に、ポインタp_numにnumのアドレスを代入します。これはアドレス演算子&を使用して行います。
p_num = #
この時点で、p_numが指す先(つまりnum)と同じ値が格納されていることになります。この状態で*p_numという形式で参照することで、num自体の値を取得することが出来ます。
*p_numはnumの値を参照しているため、以下のように*p_num = 10;
のような形式で値を代入した場合、numの値が変更されます。
int num = 5;
int *p_num;
p_num = #
*p_num = 10;
printf("%d\n",num) // 出力:10
この場合はp_numが指す先(つまりnum)自体の値が10に書き換わります。
以上が基本的なポインタを使った値の代入方法です。
配列とポインタの関係性について
配列とポインタは密接に関係しています。実際、C言語では、配列名自体がその先頭要素へのポインタとして解釈されます。
例えば、以下のようなint型の配列を考えてみましょう。
int array[5] = {1, 2, 3, 4, 5};
この場合、array
は先頭要素であるarray[0]
へのポインタとして扱われます。つまり、
printf("%d\n", *array); //出力:1
と書くことで、最初の要素である1が出力されます。
配列は、添字(を使って個々の要素にアクセスすることも可能です。例えば、
printf("%d\n", array[2]);
は3番目(0から数えた場合)の要素である3を出力します。
さらに、ポインタ演算子を使って配列内を移動することも可能です。例えば、
printf("%d\n", *(array + 2));
こちらも同じく3番目(0から数えた場合)の要素である3を出力します。
この式では、「(array + 2)
」が「&array[2]
」と同じ処理を行います。
以上より、C言語では配列名自体がその先頭要素へのポインタとして解釈されるため、「配列」と「ポインタ」は密接に関係していることが分かったかと思います。
構造体とポインタの関係性について
構造体とは、複数のデータ型をまとめたものです。ポインタと組み合わせることで、より柔軟なプログラミングが可能になります。
例えば、以下のような構造体があった場合、
struct Person {
char name[20];
int age;
};
この構造体を使って、複数人分の情報を管理することができます。通常、個々の要素にアクセスする際には、「.」演算子を使用します。
struct Person person1 = {"John", 25};
printf("%s is %d years old.\n", person1.name, person1.age);
これでは面倒くさい場合や可読性が低い場合もあります。そこでポインタを使ってアクセスする方法があります。
struct Person *pPerson;
pPerson = &person1;
printf("%s is %d years old.\n", pPerson->name, pPerson->age);
「->」演算子を使用して、ポインタから直接要素にアクセスすることができます。また、「*」演算子でも同じようにアクセスすることも可能です。
printf("%s is %d years old.\n", (*pPerson).name, (*pPerson).age);
以上のように、ポインタを使って構造体内部の要素へ簡単かつ効率的にアクセスすることが出来ます。
動的メモリ確保と解放について(malloc, free)
動的メモリ確保と解放は、プログラム実行中に必要なだけのメモリを確保し、不要になったら解放することです。
C言語では、静的メモリ(グローバル変数やstatic変数)やスタック領域(関数内の自動変数)はコンパイル時に決定されますが、動的メモリは実行時に必要な分だけ確保することができます。
malloc関数
malloc関数は、指定したバイトサイズ分の領域を動的に確保します。malloc関数を使う場合は以下のような書式で記述します。
ポインタ変数 = (型 *) malloc(バイトサイズ);
例えばint型の配列10個分の領域を確保する場合は以下のように書きます。
//実際に使うには、stdlib.hのインクルードが必要
int *p;
p = (int *) malloc(sizeof(int) * 10);
free関数
free関数は、malloc関数で確保した領域を解放します。free関数を使う場合は以下のような書式で記述します。
free(ポインタ変数);
例えば先ほど作成した配列pを解放する場合は以下のように書きます。
free(p);
不要になったメモリをfree関数で開放しないとメモリリークの原因になるので注意しましょう
注意点として、一度mallocで取得したポインターから別々に再度mallocしてもアドレス空間上連続しないため注意が必要です。またfree後もそのポインター自体が残っているため使用しないよう気をつけましょう。
二重ポインタ
二重ポインタとは、単純なポインタ変数を指すためのもう一つのポインタ変数です。このようにすることで、関数内部で引数として渡された値を書き換えることが可能になります。
以下は二重ポインタを使用した例です。
#include <stdio.h>
void swap(int **x, int **y) {
int *tmp = *x;
*x = *y;
*y = tmp;
}
int main() {
int a = 1, b = 2;
int *pa = &a, *pb = &b;
printf("before: a=%d, b=%d\n", a, b);
swap(&pa, &pb);
printf("after: a=%d, b=%d\n", a, b);
return 0;
}
このプログラムでは、swap()
関数内部で *x
を *y
と入れ替える処理が行われています。そして main()
関数内部でも同じアドレスを持った pa
, pb
の値が入れ替わっています。