第10章 ポインタ(1)
プログラミングに限ることではありませんが、学習を進めていく過程において、対象となる事物を見る目は刻々と変化していきます。数値などを格納するための箱と考えてきた変数(オブジェクト)も、本章では、記憶域の一部を占有するオブジェクトとして捉え直す。そして、いよいよC言語習得上の難関の一つといわれるポインタの学習へと進む。
10-1 ポインタ
C言語プログラミングを学ぶ上で避けて通れないのが、「オブジェクトを指す」という特殊な働きをするポインタの学習である。
関数の引数
本章の最初に考えるのは、二つの整数の和と差を求めるList 10-1のプログラムである。
List 10-1
// 二つの整数の和と差を求める(誤り)
#include <stdio.h>
//--- n1とn2の和と差をsumとdiffに格納(誤り) ---//
void sum_diff(int n1, int n2, int sum, int diff)
{
sum = n1 + n2; // 和
diff = n1 > n2 ? n1 - n2 : n2 - n1; // 差
}
int main(void)
{
int a, b;
int wa = 0, sa = 0;
puts("二つの整数を入力せよ。");
printf("整数A : "); scanf("%d", &a);
printf("整数B : "); scanf("%d", &b);
sum_diff(a, b, wa, sa);
printf("和は%dで差は%dです。\n", wa, sa);
return 0;
}
実行例
ゼロのまま!関数sum_diffを呼び出して、和と差を求めているはずですが、いずれも0となっています。関数の中で仮引数sumやdiffの値を変更しても、オリジナルのwaとsaの値に影響は及びません。値渡しによる引数の受渡しが一方通行だからです(p.150)。
Note
main関数から関数sum_diffを呼び出す際に渡されるのが、実引数aとbとwaとsaの値であって、仮引数はコピーにすぎないことを思い出しましょう。
和と差の2値を関数sum_diffに返却させればよいのでは、という考えもNGです。関数が呼出し元に戻す返却値は、たかだか1個だからです(p.146)。
この問題の解決には、C言語の難関の一つであるポインタ(pointer)の習得が必要である。本章では、ポインタの基本を学習していく。
オブジェクトとアドレス
ポインタの学習に入る前に、第2章で簡単に学習した、オブジェクト(object)そのものについて、もう少し理解を深める。
これまでは、数値などを格納するための変数=オブジェクトは、Fig.10-1 a のようにバラバラの箱と考えてきた。
ところが、実際は、図bに示すように記憶域(メモリ空間)の一部なのです。
Fig.10-1 オブジェクト
Note
オブジェクトには、数多くの性質や属性があることを思い出しましょう。たとえば、その一つが大きさです。この図ではint型のnとdouble型のxは、異なる大きさで表現されています。それぞれは、sizeof(n)とsizeof(x)で求められるのでした(処理系によっては、たまたまsizeof(int)とsizeof(double)が等しいこともあるでしょうが、第7章で学習したように、それを構成するビットの意味が異なります)。
なお、表現できる数値の範囲などを含めた型も性質の一つです。さらに、記憶域上に存在する生存期間を表す記憶域期間(第6章)も重要な性質です。
広大な空間に数多くのオブジェクトが雑居しているのですから、それぞれの〈場所〉を何らかの方法で表す必要があります。私たちの住まいと同様、場所を表すのが番地です。その番地は、アドレス(address)と呼ばれます。
重要
オブジェクトのアドレスは、記憶域上のどこに格納されているのかを表す番地のことである。
図bでは、int型オブジェクトnのアドレスが212で、double型オブジェクトxのアドレスが216です。
Note
addressには、『演説』『住所』『番地』など、多くの意味があります。
アドレス演算子 &
それでは、オブジェクトのアドレスを調べてみましょう。List 10-2に示すのが、そのプログラムです。
List 10-2
// オブジェクトのアドレスを表示する
#include <stdio.h>
int main(void)
{
int n;
double x;
int a[3];
printf("n のアドレス:%p\n", &n);
printf("x のアドレス:%p\n", &x);
printf("a[0]のアドレス:%p\n", &a[0]);
printf("a[1]のアドレス:%p\n", &a[1]);
printf("a[2]のアドレス:%p\n", &a[2]);
return 0;
}
実行結果一例
Note
表示されるアドレスの桁数や桁数などは、処理系や実行環境によって異なります(多くの環境では、4~8桁程度の16進数です)。また、実行例と図に示すアドレスの値は、あくまでも一例にすぎません(今後も、こだわらずに適当な値を示していきます)。
オペランドのアドレス取得のために使っているのが、アドレス演算子(address operator)と呼ばれる単項&演算子(unary & operator)です(Table 10-1)。
Table 10-1 単項&演算子(アドレス演算子)
注意
2項&演算子=ビット単位のAND演算子(p.202)と混同しないようにしましょう。
さて、オブジェクトnの大きさが2であって、212番地から213番地にまたがって格納されていれば、&nの評価で得られるのは、先頭アドレスの212番地です。
なお、register記憶域クラス指定子(p.175)付きで宣言されたオブジェクトに対して、アドレス演算子&を適用することはできません。
そのため、次に示すプログラムは、翻訳時にエラーとなります("chap10/register.c")。
本プログラムでは、変数nとxと、配列aの全要素のアドレスを表示しています。
重要
オブジェクトのアドレスは、アドレス演算子&で取り出せる。
なお、この演算子で取得したアドレスを表示する際の変換指定は%pです。
Note
変換指定%pのpは、pointerに由来します。
Column 10-1:バイト順序
第7章では、char型が1バイトであることや、それ以外の型のオブジェクトが複数のバイトで構成されることを学習した。実は、記憶域上にバイトを並べる順序は、処理系に依存する。
それを表したのが、Fig.10C-1である(この図は、int型が2バイト16ビットであるとしている)。
リトルエンディアン
下位バイトが先頭側(低アドレス)に配置される。リトルエンディアン(little endian)と呼ばれる方法である。Intel社のパソコン用のCPUで採用されている。
ビッグエンディアン
下位バイトが末尾側(高アドレス)に配置される。ビッグエンディアン(big endian)と呼ばれる方法である。
エンディアンの語源
エンディアンという用語は、Jonathan Swiftの1726年の小説『ガリバー旅行記』で、小人国では「卵は太いほうから割るべきだ。」とするビッグエンディアンと「卵は細いほうから割るべきだ。」とするリトルエンディアンとが対立する話に由来する。1981年に、Danny Cohenの"On holy wars and a plea for peace"によって、この言葉がコンピュータの世界に導入された。
ポインタは、オブジェクトの先頭番地を指す。そのため、リトルエンディアンであればポインタは下位バイトを指し、ビッグエンディアンでは上位バイトを指すことになる。
Fig.10C-1: バイト順序とエンディアン
この図は、同じデータがメモリ上でどのように配置されるかを、リトルエンディアンとビッグエンディアンの両方式で示している。
ポインタ
オブジェクトのアドレスを表示するだけでは、何の役にも立ちません。本章のメインテーマであるポインタを使うと、アドレスを有効活用できます。List 10-3で学習しましょう。
List 10-3
// ポインタ(アドレス演算子&と間接演算子*)
#include <stdio.h>
int main(void)
{
int n = 57;
printf("n = %d\n", n);
printf("&n = %p\n", &n);
int *p = &n; /*1*/
printf("p = %p\n", p); /*2*/
printf("*p = %d\n", *p); /*3*/
return 0;
}
実行結果
1の宣言では、型名と変数名のあいだにが置かれています。この宣言によって、変数pの型は、『int型オブジェクトへのポインタ型』となります。なお、型名があまりにも長いため、『intへのポインタ型』あるいは『int 型』と省略して呼ぶのが一般的です。
さて、与えられている初期化子が&nですので、pは変数nのアドレスで初期化されます。このときのpとnの関係は、次のように表現されます。
重要
ポインタpの値がnのアドレスであるとき、『pはnを指す』という。
ポインタがオブジェクトを指すイメージを表したのが、Fig.10-3 aです。
- 矢印の始点 … ポインタ
- 矢印の終点 … そのポインタによって指されているオブジェクト
さて、変数pの型が『int *型』ですから、初期化子の&nも、同じ型のはずです。
Fig.10-3 オブジェクトとポインタ
ここまで、&演算子は『アドレスを取得する演算子』と考えてきましたが、より正確には、『ポインタを生成する演算子』です(Table 10-1:p.278)。
式&nは、nを指すポインタであり、その評価で得られるのがnのアドレス、というわけです。
重要
Type型のオブジェクトnに対してアドレス演算子&を適用したアドレス式&nは、Type *型のポインタであり、その値はnのアドレスである。
ポインタの値は、指しているオブジェクトのアドレスですから、2で表示されるpの値は、pが指しているnのアドレスとなります。
注意
ポインタの宣言時に注意すべき点があります。次のように宣言すると、変数p2は、ポインタでなく、たんのint型になってしまうことです。
p2もポインタとして宣言するのであれば、p2の前にも*を置く必要があります。間接演算子 *
次は、3に進みます。ここで使っているのが、間接演算子(indirect operator)と呼ばれる単項演算子(unary * operator)です。Table 10-2に示すように、ポインタに間接演算子を適用した間接式は、そのポインタが指すオブジェクトそのものを表す式となります。
Table 10-2 単項*演算子(間接演算子)
間接式*pが、nそのものを表すことをイメージしたのが、Fig.10-3 bです。
このように、『式pが、nそのものを表す』ことを、『pはnのエイリアス(alias)である』と表現します。エイリアスは、『別名』『あだ名』という意味です。
変数nに対して、*pという『あだ名』が与えられたと考えましょう。
重要
Type 型ポインタpがType型オブジェクトnを指すとき、間接演算子を適用した間接式*pは、nのエイリアス(別名/あだ名)となる。
Note
Fig.10-3のaとbが表すのは、同じ状態です。『ポインタがオブジェクトを指す』イメージが図a で、『ポインタに間接演算子を適用した間接式が、指し先のエイリアスとなる』イメージが図b です。
ポインタに間接演算子を適用することで、ポインタが指すオブジェクトを間接的にアクセスすることは、参照外しと呼ばれます。
注意
ポインタが何も指していない状態で参照外しを行うと、思いもよらぬ結果につながります。ポインタに対しては、初期化あるいは代入によって、オブジェクトへのポインタ(アドレス)を入れたうえで、参照外しを行う必要があります。
アドレス演算子&と間接演算子*について、List 10-4で理解を深めましょう。
List 10-4
// ポインタの指す先を実行時に決定
#include <stdio.h>
int main(void)
{
int x = 123;
int y = 456;
int sw;
printf("x = %d\n", x);
printf("y = %d\n", y);
printf("変更するのは [0…x / 1…y] = ");
scanf("%d", &sw);
int *p;
if (sw == 0)
p = &x; // pはxを指す
else
p = &y; // pはyを指す
*p = 999; /*2*/
printf("x = %d\n", x);
printf("y = %d\n", y);
return 0;
}
実行例1
実行例2
xとyの値を表示した後に、どちらの値を変更するのかの選択が促されます。 1では、選択結果に応じて、&xと&yのいずれかをポインタpに代入し、続く2では、ポインタpが指すオブジェクト*pに999を代入しています。
二つの実行例と、Fig.10-4とを見比べながら、理解していきましょう。
実行例1=図a:ポインタpに&xが代入されるため、pはxを指します。その状態でpに999を代入します。pはxのエイリアスであり、999の代入先はxです。
実行例2=図b:ポインタpに&yが代入されるため、pはyを指します。その状態でpに999を代入します。pはyのエイリアスであり、999の代入先はyです。
Fig.10-4 配列の要素を指すポインタと要素のエイリアス
プログラム上で直接的には値が代入されていない、変数xあるいはyの値が更新されるのは、ちょっと不思議な感じがします。
しかも、2の『*p = 999;』というコードからは、999の代入先は特定できません。アクセス先(読み書き先)の決定が、プログラムのコンパイル時に静的(スタティック)に行われるのではなく、プログラムの実行時に動的(ダイナミック)に行われるからです。
重要
ポインタをうまく活用すれば、アクセス先の決定を、プログラム実行時に動的に行うコードが実現できる。
Note
"静的な (static)" は、時間が経過しても変化しないことを、"動的な (dynamic)" は、時間の経過とともに変化することを意味します。
なお、ポインタの宣言と1をまとめると、簡潔になります("chap10/list1004a.c")。
それでは、アドレス演算子&を適用したアドレス式と、間接演算子*を適用した間接式について、評価がどのようになるのかをFig.10-5を見て確認しましょう。
Note
この図は、実行例1のように、pがxを指しているときに、*pに999が代入された後の状態です。
Fig.10-5 アドレス式と間接式の評価
10-2 ポインタと関数
C言語のプログラムで、ポインタの利用を避けられない局面の一つが、関数の引数としてのポインタです。本節では、関数の引数としてのポインタについて学習します。
関数の引数としてのポインタ
先ほどのプログラムは、目的とする変数に999を代入するものでした。その働きを関数として実現することを考えましょう。
もちろん、右に示す関数はNGです。本章の冒頭で復習したように、コピーにすぎない仮引数の値を変更しても、呼出し側の実引数に反映されないからです。
呼び出し側が、『この番地に入っている変数の値を変更してください』と依頼すればよさそうです。そのように作ったのが、List 10-5に示すプログラムです。
List 10-5
// ポインタによって値の変更を依頼
#include <stdio.h>
//--- pが指す変数に999を代入 ---//
void set999(int *p)
{
*p = 999;
}
int main(void)
{
int x = 123;
int y = 456;
int sw;
printf("x = %d\n", x);
printf("y = %d\n", y);
printf("変更するのは [0…x / 1…y] = ");
scanf("%d", &sw);
if (sw == 0)
set999(&x); // xの変更を依頼 /*1*/
else
set999(&y); // yの変更を依頼 /*2*/
printf("x = %d\n", x);
printf("y = %d\n", y);
return 0;
}
実行例1
実行例2
実行例1で、1の関数呼出し式set999(&x)によって関数set999を呼び出したときの挙動を、Fig.10-6を見ながら理解していきましょう。
Fig.10-6 関数呼出しにおけるポインタの受渡し
まずは、関数set999の仮引数pの宣言に着目します。『int *p』となっており、仮引数pが『intへのポインタ型』であることが分かります。
そのポインタpに対して、呼出し側が与えた実引数&xの値(xの格納番地)がコピーされるのですから、次の状態となります。
pはxを指す。
関数本体で行う『p = 999;』の代入は、前のプログラムと同じです。式pはxのエイリアス(別名)であって、*pへの代入は、xへの代入を意味します。
そのため、関数set999からmain関数に戻った後も、xにはちゃんと999が入っています。
関数に対して、変数の値の変更を頼みたいときは、その変数へのポインタを実引数として与えて、次のように依頼すればよいことが分かりました。
ポインタを渡しますので、そのポインタが指すオブジェクトに対して処理を行って、値を書きかえてください!!
呼び出された側の関数では、仮引数に受け取ったポインタに間接演算子*を適用することによって、そのポインタが指すオブジェクトが間接的に扱えます。
このことからも、単項&演算子が、間接演算子と呼ばれる理由がよく分かるでしょう。
演習10-1
nの指す値が0より小さければ0に更新し、100より大きければ100に更新する(値が0~100であれば更新しない)関数adjust_pointを作成せよ。
和と差を求める関数
本章の冒頭では、二つの整数の和と差を求める(誤った)プログラムの問題点を考えていました。その問題を解決するように書きかえたのが、List 10-6のプログラムです。
List 10-6
// 二つの整数の和と差を求める
#include <stdio.h>
//--- n1とn2の和と差を*sumと*diffに格納 ---//
void sum_diff(int n1, int n2, int *sum, int *diff)
{
*sum = n1 + n2;
*diff = n1 > n2 ? n1 - n2 : n2 - n1;
}
int main(void)
{
int a, b;
int wa = 0, sa = 0;
puts("二つの整数を入力せよ。");
printf("整数A : "); scanf("%d", &a);
printf("整数B : "); scanf("%d", &b);
sum_diff(a, b, &wa, &sa);
printf("和は%dで差は%dです。\n", wa, sa);
return 0;
}
実行例
関数sum_diffの仮引数の宣言に着目します。n1とn2はint型のままですが、sumとdiffがint *型のポインタに変更されています。
呼出し側では、sumとdiffに対して、実引数として&waと&saを与えています。Fig.10-7に示すように、waとsaのアドレスがコピーされるため、sumはwaのエイリアスとなり、diffはsaのエイリアスとなります。
関数本体では、求めた和をsumに代入して、差をdiffに代入します。これらの代入は、waとsaへの代入を意味しますので、main関数に戻った後も、ちゃんと、waには和が格納され、saには差が格納されている状態となります。
Fig.10-7 引数とポインタ
重要
オブジェクトへのポインタを仮引数に受け取れば、そのポインタに間接演算子*を適用することによって、そのオブジェクトそのものにアクセスできる。これを利用すると、呼出し元が用意したオブジェクトの値を呼び出された側で変更できる。
これで、一件落着です。
2値の交換
次は、二つの値を交換する関数を作りましょう。もちろん、関数が受け取る仮引数はポインタでなければなりません。List 10-7に示すのが、そのプログラムです。
List 10-7
// 二つの整数値を交換する
#include <stdio.h>
//--- 2値の交換(xとyが指すオブジェクトの値の交換)---//
void swap(int *x, int *y)
{
int temp = *x;
*x = *y;
*y = temp;
}
int main(void)
{
int a, b;
puts("二つの整数を入力せよ。");
printf("整数A : "); scanf("%d", &a);
printf("整数B : "); scanf("%d", &b);
swap(&a, &b);
puts("これらの値を交換しました。");
printf("整数Aは%dです。\n", a);
printf("整数Bは%dです。\n", b);
return 0;
}
実行例
関数swapの挙動を、Fig.10-8を見ながら理解していきましょう。引数のやりとりの結果、仮引数であるポインタxがaを指して、ポインタyがbを指すことは、分かるでしょう。
関数本体で行っているのは、xの値とyの値の交換です。この交換は、実質的に、main関数のaとbの値の交換です。
Fig.10-8 引数とポインタ
Note
2値の交換の手順は、第5章で学習しました(p.123)。今回は、交換の対象が、xとyではなく、xとyとなっています(tempはint型整数ですから、*tempなどとしてはなりません)。
演習10-2
西暦y年m月d日の日付を、"前の日"あるいは"次の日"の日付に更新する関数を作成せよ。
閏年を考慮して計算を行うこと。
2値のソート
前ページで作成したswapを応用すると、二つの整数値をソートするプログラムが作れます。List 10-8に示すのが、そのプログラムです。
List 10-8
// 二つの整数を昇順に並べる
#include <stdio.h>
//--- xとyが指すオブジェクトの値を交換 ---//
void swap(int *x, int *y)
{
int temp = *x;
*x = *y;
*y = temp;
}
//--- *n1≦*n2となるようにソート ---//
void sort2(int *n1, int *n2)
{
if (*n1 > *n2)
swap(n1, n2); /*1*/
}
int main(void)
{
int a, b;
puts("二つの整数を入力せよ。");
printf("整数A : "); scanf("%d", &a);
printf("整数B : "); scanf("%d", &b);
sort2(&a, &b); /*2*/
puts("昇順にソートしました。");
printf("整数Aは%dです。\n", a);
printf("整数Bは%dです。\n", b);
return 0;
}
実行例
Note
2値のソートを行うのが関数sort2であり、その中で関数swapが呼び出される構造です。
Fig.10-9を見ながら理解していきましょう。
Fig.10-9 2値のソート
main関数から関数sort2を呼び出す2の『sort2(&a, &b);』では、実引数として&aと&bを与えることで、二つの変数aとbの値の変更を依頼しています。
呼び出された関数sort2は、aへのポインタとbへのポインタを、仮引数n1とn2に受け取ります。この関数が行うことは、n1が指す変数の値n1と、n2が指す変数の値n2を昇順にソートすることです。
ただし、n1がn2の値以下であれば、2値はソート済みということですから、何も行う必要がありません。
そうでないとき、すなわち、n1の値がn2の値より大きいときは、2値の交換が必要です。そこで、関数swapを呼び出すことで、2値の交換を依頼します。
そのために行っているのが、1の関数呼出し『swap(n1, n2);』です。関数swapに与えている実引数n1とn2には、(値の変更を依頼するにもかかわらず)アドレス演算子&が適用されていません。
その理由を考えていきましょう。
関数sort2の仮引数n1とn2には、aとbへのポインタがコピーされています。そのため、n1の値はaのアドレス、n2の値はbのアドレスとなっています。そのアドレスをそのまま関数swapに渡して、次のように依頼しているのです。
212番地に入っている整数と、216番地に入っている整数の値を交換してください!
Note
関数sort2は、受け取ったポインタを、そのまま関数swapに『たらい回し』しているわけです。なお、変数n1とn2にアドレス演算子&を適用してswap(&n1, &n2)とすると、関数swapに渡されるのが、変数aとbのアドレスではなく、変数n1とn2のアドレスとなってしまいます。
scanf関数とポインタ
第1章でscanf関数を初めて使ったときのことを思い出しましょう(p.16)。
printf関数による表示とは異なって、scanf関数による読込みでは、実引数として与える変数名の前に&を付ける必要があることを学習しました。
scanf関数は、呼出し側の関数が用意したオブジェクトに値を格納しなければならないため、変数の〈値〉をもらっても仕方ありません。ポインタを受け取ることで、そのポインタが指すオブジェクトに対して、キーボードから読み込んだ値を格納するのです。
そのため、scanf関数を呼び出す側では、
このアドレスに格納されているオブジェクトに読み込んだ値を入れてください!!
と依頼するために、アドレス演算子&を変数に適用した上で渡す必要があります。
念のために、printf関数への依頼と、scanf関数への依頼をFig.10-10で対比しましょう。
Fig.10-10 printf関数の呼出しとscanf関数の呼出し
空ポインタ
オブジェクトを指すポインタと明確に区別可能な、何も指さないことが保証されている、空ポインタ(null pointer)と呼ばれる特別なポインタがあります。
空ポインタを表すオブジェクト形式マクロが、空ポインタ定数(null pointer constant)という名称のNULLです。
重要
何も指さない特別なポインタが空ポインタであり、それを表すオブジェクト形式マクロNULLは空ポインタ定数である。
空ポインタ定数NULLは
次に示すのが、空ポインタ定数NULLの定義の一例です。
Note
空ポインタを実際に利用するプログラムや演習は、この後の章で学習します。
スカラ型
番地を表すポインタは、一種の数量とみなせます。第7章で学習した算術型と、本章で学習したポインタ型をあわせてスカラ型(scalar type)と呼びます。
Note
scalarとは、『数』、あるいは『数と同等な性質をもつ量』のことです。スカラに大きさはありますが、方向はありません(方向をもつのはvectorです)。
Column 10-2 ポインタの型
次のようにscanf関数を呼び出したらどうなるかを検討しましょう。
なお、ここではdouble型が8バイトで、int型が2バイトであるとして考えていきます。
double型の値を読み込むように指示されたscanf関数は、受け取ったアドレスを先頭にした8バイトの領域に対して、キーボードから読み込んだ値を書き込もうとします(右図)。
Fig.10C-2 scanf関数
しかし、変数xは2バイト分の領域しかありませんので、変数用の領域を超えて書込みが行われることになります。
次は、List 10-8のプログラムの関数swapを、次のように呼び出すことを考えましょう。
交換すべき領域は1バイトのはずですが、sizeof(int)バイトの交換が行われるため、先ほどと同じような問題が発生します。
※多くの処理系では、コンパイル時に警告メッセージが出力されます。
Type へのポインタ、すなわち、Type *型ポインタは、たんに、『○○番地』を指すのではなく、『○○番地を先頭に格納されたType型のオブジェクト』を指すのです。
特殊なテクニックを利用するケースを除き、Type *型ポインタが、Type以外の型のオブジェクトを指すようなことは避けなければなりません。
本書は入門書ですので、オブジェクトを指すポインタのみを学習しました。C言語のポインタには、関数を指すポインタもあります。
演習問題
目次
演習問題12-1:範囲制限関数
問題の説明
指す値が0より小さければ0に更新し、100より大きければ100に更新する関数adjust_point
を作成してください。この関数は、点数などの値を有効範囲(0~100)内に収めるために使用します。
期待される結果
入力例1:
出力例1:
入力例2:
出力例2:
入力例3:
出力例3:
ヒント
- ポインタ変数が指す値を変更するには、間接演算子
*
を使います - 条件分岐を使って3つの場合(0未満、100超過、範囲内)を処理します
- 関数の戻り値は
void
なので、値の変更はポインタを通じて行います - 以下はmain関数の全体コードです:
演習問題12-2:ポインタによる二乗計算
問題の説明
整数値へのポインタを受け取り、その値を二乗して結果を元の変数に格納する関数square_value
を作成してください。
期待される結果
入力例1:
出力例1:
入力例2:
出力例2:
入力例3:
出力例3:
ヒント
- ポインタの値を使って計算するには間接演算子
*
を使います - 計算結果は元の変数に格納するため、間接演算子を使って代入します
- 計算時に優先順位に注意し、必要に応じて括弧を使用してください
- 以下はmain関数の全体コードです:
演習問題12-3:三数ソート
問題の説明
三つのint型整数を昇順にソートする関数sort3
を作成してください。
期待される結果
入力例1:
出力例1:
入力例2:
出力例2:
入力例3:
出力例3:
ヒント
- 三つの数を昇順に並べるには、比較と交換を複数回行う必要があります
- 二つの数の交換には、補助関数
swap
を使うと便利です - 交換の順序を考えて、すべての場合に対応できるようにしましょう
- n1とn2を比較、必要なら交換(大きい方をn2に移動)、n2とn3を比較、必要なら交換(大きい方をn3に移動)、n1とn2を再度比較、必要なら交換 (前のステップでn2が変わった可能性があるため)
- 以下はmain関数の全体コードです:
演習問題12-4:二次方程式の解法
問題の説明
二次方程式 ax² + bx + c = 0 の解を求める関数を作成してください。実数解がある場合はその値と解の個数を、解がない場合は解の個数を0として返します。
期待される結果
入力例1:
出力例1:
入力例2:
出力例2:
入力例3:
出力例3:
ヒント
- 二次方程式の解は判別式 D = b² - 4ac を使って判定します
- 判別式の値により、解の個数(0, 1, 2)が決まります
- 解の値はポインタを使って呼び出し元に返します
- 関数の戻り値は解の個数とします
- 以下はmain関数の全体コードです:
#include <stdio.h> #include <math.h> // sqrt関数の使用には math.h のインクルードが必要です。 // コンパイル時に -lm オプションが必要な環境もあります int main(void) { double a, b, c; double x1, x2; int solutions; printf("二次方程式 ax² + bx + c = 0 の係数を入力してください:\n"); printf("a = "); scanf("%lf", &a); if (a == 0) { printf("aが0の場合、二次方程式ではありません。\n"); return 1; } printf("b = "); scanf("%lf", &b); printf("c = "); scanf("%lf", &c); solutions = solve_quadratic(a, b, c, &x1, &x2); printf("\n方程式 %.2fx² + %.2fx + %.2f = 0 の解:\n", a, b, c); switch (solutions) { case 0: printf("実数解はありません\n"); break; case 1: printf("重解 x = %.4f\n", x1); break; case 2: printf("x₁ = %.4f\n", x1); printf("x₂ = %.4f\n", x2); break; } return 0; }
C言語におけるポインタの重要性と歴史的背景
概要
C言語は、1970年代初頭にデニス・リッチー(Dennis Ritchie)によって設計され、BCPLおよびB言語に由来する「アドレスはデータである」という思想を継承しています。この設計思想により、プログラムはメモリアドレスをデータとして操作できるようになり、C言語は当時のシステムプログラミングの要求に応えることができました。
歴史的背景
Unixオペレーティングシステムの開発においては、ハードウェアレジスタやカーネル内のデータ構造に直接アクセスする必要がありました。C言語のポインタ機能により、アセンブリ言語のように直接メモリを読み書きしながらも、高水準言語としての表現力と構造性を維持することができたのです。
この「低レベルな操作」と「高レベルな表現力」の両立こそが、C言語がOS開発やデバイスドライバ、組み込みシステムといった低レイヤー開発に広く用いられる理由です。
基本的なポインタの概念
ポインタとは
ポインタは、他の変数のメモリアドレス(場所)を格納する変数です。「指し示す」という意味の通り、メモリ上の特定の場所を「指す」ことができます。
ポインタは単なる歴史的遺産ではなく、C言語における中核的な機能の一つです。プログラムの性能と柔軟性の向上において不可欠な役割を果たしています。
1. メモリへの直接アクセス
直接アクセスの利点
ポインタは変数やハードウェアアドレスを保持し、プログラムがメモリ内容を直接読み書きできるようにします。これは、特にシステムプログラミングやデバイスドライバの開発において不可欠です。
メモリ直接アクセスの例
2. 処理効率の向上
効率性の重要性
関数に大型データ構造を渡す際、ポインタを利用すればデータのコピーを避けることができ、メモリ使用量と処理時間を削減できます。特にリソースの限られた環境では大きな利点となります。
効率的なデータ渡しの比較
#include <stdio.h>
struct LargeStruct {
int data[1000];
char info[500];
};
// 値渡し方式 - 構造体全体をコピーする
void processStruct(struct LargeStruct s) {
s.data[0] = 999; // コピーのみを変更、元データには影響しない
}
// ポインタ渡し方式 - アドレスのみを渡す、効率的で元データを変更可能
void processStructPtr(struct LargeStruct *s) {
s->data[0] = 999; // 元データを直接変更
}
int main() {
struct LargeStruct myStruct = {0};
printf("変更前: %d\n", myStruct.data[0]);
processStructPtr(&myStruct);
printf("変更後: %d\n", myStruct.data[0]);
return 0;
}
3. 動的メモリ管理の実現
動的メモリ管理
malloc()
やfree()
といった関数と組み合わせることで、実行時に必要なメモリを動的に確保・解放できます。これにより、リストや木構造、可変長の入力データなど、柔軟なデータ構造の構築が可能になります。
動的メモリ確保の例
#include <stdio.h>
#include <stdlib.h>
int main() {
int n = 5;
// 動的にメモリを確保
int *arr = (int*)malloc(n * sizeof(int));
if (arr == NULL) {
printf("メモリ確保に失敗しました\n");
return 1;
}
// 配列を初期化
for (int i = 0; i < n; i++) {
arr[i] = i * 10;
}
// 配列を出力
for (int i = 0; i < n; i++) {
printf("arr[%d] = %d\n", i, arr[i]);
}
// メモリを解放
free(arr);
return 0;
}
4. 配列と文字列の効率的な操作
配列とポインタの関係
多くの文脈で、配列名はその先頭要素へのポインタに自動的に変換されるため、ポインタを使って配列やヌル終端文字列(\0
)を効率的に扱うことができます。
配列とポインタの操作例
#include <stdio.h>
#include <string.h>
int main() {
char str[] = "Hello, World!";
char *ptr = str; // 文字列の最初の文字を指す
// ポインタを使って文字列を走査
printf("文字ごとの出力: ");
while (*ptr != '\0') {
printf("%c", *ptr);
ptr++;
}
printf("\n");
// 配列操作の例
int arr[] = {10, 20, 30, 40, 50};
int *arrPtr = arr;
printf("配列要素: ");
for (int i = 0; i < 5; i++) {
printf("%d ", *(arrPtr + i)); // ポインタ演算
}
printf("\n");
return 0;
}
5. 複雑なデータ構造の実現
データ構造の柔軟性
リンクリスト、二分木、グラフなどの構造体は、ノード同士をポインタで接続することにより、柔軟なメモリ配置と動的な構造拡張が可能になります。
リンクリストの実装例
#include <stdio.h>
#include <stdlib.h>
// リンクリストのノード構造
struct Node {
int data;
struct Node *next;
};
// 新しいノードを作成
struct Node* createNode(int data) {
struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
newNode->data = data;
newNode->next = NULL;
return newNode;
}
// リンクリストを出力
void printList(struct Node* head) {
struct Node* current = head;
while (current != NULL) {
printf("%d -> ", current->data);
current = current->next;
}
printf("NULL\n");
}
int main() {
// 簡単なリンクリストを作成
struct Node* head = createNode(1);
head->next = createNode(2);
head->next->next = createNode(3);
printf("リンクリストの内容: ");
printList(head);
// メモリを解放(簡略版)
free(head->next->next);
free(head->next);
free(head);
return 0;
}
6. 関数ポインタと参照渡し
関数ポインタ
関数ポインタの利点
関数ポインタを用いれば、実行時に呼び出す関数を選択することができ、コールバックや戦略パターンの実装など、柔軟なモジュール設計が可能になります。
関数ポインタの使用例
#include <stdio.h>
// いくつかの簡単な数学関数
int add(int a, int b) { return a + b; }
int multiply(int a, int b) { return a * b; }
int main() {
// 関数ポインタを宣言
int (*operation)(int, int);
// 異なる関数を指す
operation = add;
printf("5 + 3 = %d\n", operation(5, 3));
operation = multiply;
printf("5 * 3 = %d\n", operation(5, 3));
return 0;
}
参照渡し
参照渡しの効果
参照渡しでは、変数のアドレスを関数に渡すことで、関数内の変更が呼び出し元に反映され、値渡しの制限を補うことができます。
参照渡しの例
使用上の注意点
注意
ポインタは強力である一方で、誤用すると以下のような深刻な問題を引き起こす可能性があります:
- メモリリーク(確保したメモリの解放忘れ)
- ダングリングポインタ(無効なアドレスを指すポインタ)
- 境界外アクセス(配列外や無効領域へのアクセス)
メモリリークの問題
メモリリークの例と対策
#include <stdio.h>
#include <stdlib.h>
// 間違った例 - メモリリーク
void memoryLeakExample() {
int *ptr = (int*)malloc(sizeof(int) * 100);
*ptr = 42;
// free(ptr)の呼び出しを忘れる - メモリリーク!
}
// 正しい例
void correctExample() {
int *ptr = (int*)malloc(sizeof(int) * 100);
if (ptr != NULL) {
*ptr = 42;
free(ptr); // メモリを正しく解放
ptr = NULL; // ダングリングポインタを避ける
}
}
ダングリングポインタの危険性
ダングリングポインタ
解放されたメモリ領域を指すポインタを「ダングリングポインタ」と呼びます。このようなポインタを使用すると未定義動作を引き起こします。
ダングリングポインタの例と対策
境界外アクセスの防止
境界チェックの重要性
配列の境界を超えたアクセスは、バッファオーバーフローと呼ばれる深刻なセキュリティ脆弱性の原因となります。この問題を防ぐには、厳密な境界チェックが不可欠です。
境界外アクセスの例と対策
#include <stdio.h>
#include <string.h>
int main() {
int arr[5] = {10, 20, 30, 40, 50}; // インデックス 0-4 のみ有効
char buffer[10];
char source[] = "Hello";
// 危険な配列アクセス
// printf("%d\n", arr[5]); // 境界外読み取り - 未定義動作
// arr[10] = 999; // 境界外書き込み - メモリ破壊
// 危険な文字列操作
// strcpy(buffer, "Hello World Long String"); // バッファオーバーフロー
// 安全な配列アクセス
int index = 4;
if (index >= 0 && index < 5) {
printf("arr[%d] = %d\n", index, arr[index]); // 安全
}
// 安全な文字列操作
if (strlen(source) < sizeof(buffer)) {
strcpy(buffer, source);
printf("コピー結果: %s\n", buffer);
}
return 0;
}
最適な方法
推奨事項
ポインタを安全に使用するための推奨事項:
- 初期化を忘れない - ポインタは宣言時に初期化する
- NULLチェック - malloc()の戻り値は必ずチェックする
- 対応するfree() - malloc()には必ずfree()を対応させる
- NULLポインタの設定 - free()後はポインタをNULLに設定する
- 境界チェック - 配列アクセス時は境界を確認する
安全なポインタ使用の例
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr = NULL; // 初期化
// メモリ確保とNULLチェック
ptr = (int*)malloc(sizeof(int) * 10);
if (ptr == NULL) {
fprintf(stderr, "メモリ確保に失敗しました\n");
return 1;
}
// 安全な使用
for (int i = 0; i < 10; i++) {
ptr[i] = i * 2;
}
// 適切な解放
free(ptr);
ptr = NULL; // ダングリングポインタを防ぐ
return 0;
}
結論
C言語におけるポインタは、システムプログラミングを支えるための設計思想から生まれた中核的な技術です。高い表現力と直接的なメモリ操作を両立することで、C言語は柔軟で高性能なプログラムを記述可能にしています。
扱いが難しい一面もありますが、適切に利用すれば、他の高水準言語にはない強力な機能を活かすことができ、今なお多くの場面で不可欠な存在となっています。
「プログラミング言語及び演習Ⅱ」
上記の内容については、藤岡先生が担当される「プログラミング言語及び演習Ⅱ」において詳しく説明します。