Week 4 (2024/10/24)¶
今日やること
- ポインタの基本
- ポインタと関数
- ポインタと配列
- ポインタと文字列
ポインタの基本¶
今日はついにポインタの日です。ポインタは、これまでにちょくちょく出てきた「アドレス」に関する処理を行う概念です。 C言語における最も重要で最も難しい概念だと言えます。ポインタを用いることで、データの移動や編集が自由自在に出来るようになります。 ソフトウェア1の目的はポインタを理解することです。 ポインタを理解することは簡単ではないですが、今日一日かけて頑張って学びましょう。
ポインタとは¶
int a = 10;
int *p; // pはintへのポインタ
p = &a; // aのアドレスをpに代入
ここで、int *p
という表記は、pはintへのポインタであることを意味します。
ポインタは他の変数のアドレスを内容とする変数です。
この関係を図示すると以下のようになります。
ポインタは、これまでに習った普通の変数と同様に、メモリ中にある領域(64ビットマシンの場合は64ビット)を確保します。
そして、この場合ではint
型の変数a
のアドレス&a
(ここでは人工的な例として0xff5c
)を値として持ちます。
これを、ポインタpはaを指しているとか、ポイントしているという風に表現します。
これを図中では矢印で表現しています。
ポインタの実体は、上の図で全てです。なので、ポインタについてわからなくなったら、上の図に立ち返ってみるといいです。
そして、ポインタには、 *をつけると、指している変数の値にアクセスできるという特殊能力があります。 アスタリスクなので「値(あたい)」と覚える人もいます。
printf("*p: %d\n", *p); // *p: 10
*p
は指し示しているa
そのものです。なので、値を表示するだけでなく、更新することもできます。
*p = 5;
printf("a: %d\n", a); // a: 5
*p += 3;
printf("a: %d\n", a); // a: 8
さて、ここで、以下のように「ポインタの値」と、「指し示している変数のアドレス」を直接表示してみましょう。 上の図で示している通り、この二つは同じものになります。
// ポインタの値(&をつけていないことに注意) と、指し示しているaのアドレスは、同じです(値は人工的なものです)
printf("p: %p, &a: %p\n", p, &a); // p: 0xff5c, &a: 0xff5c
printf("&p: %p\n", &p); // &p: 0x51a5
ここでの要点は、
- ポインタは、これまでの変数と同様に、メモリ上に領域が確保されるというもの。ポインタのことを「ポインタ変数」とも言う
- ポインタは、その要素として、「指し示している対象のアドレス」を持つ
- ポインタは、「
*
」をつけることで、「指し示している対象の要素」にアクセスできる
ということです。ちなみに、他の型に対するポインタも同様に作れます。
double *dp; // doubleに対するポインタ
double d = 3.0;
dp = &d; // OK.
int aa = 3;
// ここで dp = &aa; のように、型が違う変数を指してはダメ。
コラム
int *a; // (int *) という型のポインタ変数a
int* a; // (int*) という型のポインタ変数a
しかしこの表記には少し問題があります。「int*」を型だと思って、ポインタを三つ作ろうと思い以下のようにすると間違いです。
int* p1, p2, p3;
int *p1
, int p2
, int p3
と解釈され、p2
とp3
は普通のintになってしまいます。
よって、表記には十分注意してください。
たとえば次の表記は、
int a, b, *p;
a
とb
が普通のintで、p
がポインタになります。
ちなみに、変数がポインタかどうか調べるにはsizeof
を使えばいいです。64bit環境では、sizeof(a)
は4になり、sizeof(p)
は8になります。
コラム
c言語のドはまりポイントとして、ポインタ宣言のときにアスタリスク(*
)を使い、ポインタが指し示す変数にアクセスするときもアスタリスクを使うという点にあります。
この二つは全く別の操作なのですが、なぜかどちらにもアスタリスクが割り振られています。 すなわち、
int a = 10;
int *p; // (1) ポインタを作るときはアスタリスクをつけるというルール
p = &a;
printf("%d\n", *p); // (2) ポインタの特殊能力:アスタリスクをつけると指している変数の値にアクセス
*
を使って実現しています。
もしC言語に絵文字を使うことができれば、次のように別のシンボルを割り振ってもOKでした。
int a = 10;
int *p; // (1) ポインタを作るときはアスタリスクを付ける
p = &a;
printf("%d\n", 🍌p); // (2) ポインタの特殊能力:バナナをつけると指している変数の値にアクセス
コラム
上の2つのコラムを理解すると、次の表記も理解できると思います。 ポインタに対し、宣言したあとに値を代入するか、宣言と同時に値を代入するかで、表記にアスタリスクがつくつかないがあり わかりにくいです:
int a = 10;
int *p;
p = &a; // pに*はついてない。普通の「変数への値の代入」なので。
int a = 10;
int *p = &a; // pに*がついている。だがこれは「ポインタを宣言するときのルール」というだけ。
int *
という部分はあくまでポインタを宣言するときのルールというだけです。
int n;
n = 10;
int n = 10;
ポインタのコピー¶
さて、通常の変数はコピーをすることで値を複製することができます。 全く同様に、ポインタ変数もコピーすることができます。
int a = 10;
int *p;
p = &a; // p は aを指す
int *q; // もう一個ポインタを宣言
q = p; // q = &a と同じ意味
// ここ以降、a, *p, *q は同じ意味になる
printf("a: %d, *p: %d, *q: %d\n", a, *p, *q); // a: 10, *p: 10, *q: 10
printf("p: %p, q: %p\n", p, q); // p: 0xff5c, q: 0xff5c
ここで、p
に加えて二個目のポインタq
を作ります。q = p
という表記で、p
の中身すなわち「a
のアドレス」をq
に代入します。
これは通常の変数のコピーと同じです。結果として、q
もまたa
を指し示すことになります。
よって、a
, *p
, *q
はどれも同じ意味になるので、どれかを変更すれば残りの二者も変更されます。
*p = 13;
printf("a: %d, *p: %d, *q: %d\n", a, *p, *q); // a: 13, *p: 13, *q: 13
また、ポインタは単なる変数なので、別の値を指すように変更することも可能です。
int b = 3;
q = &b; // qの中身を、「bのアドレス」に変更
printf("a: %d, *p: %d, *q: %d\n", a, *p, *q); // a: 13, *p: 13, *q: 3
これを可視化すると次のようになります。q
は、別の変数b
を指すようになりました。
コラム
さて、p = q
と*p = *q
の違いを確認しておきましょう。次の例は、上で習った通り、ポインタ変数のコピーです。
int a = 10, b = 3, *p, *q;
p = &a;
q = &b;
printf("a: %d, b: %d, *p: %d, *q: %d\n", a, b, *p, *q); // a: 10, b: 3, *p: 10, *q: 3
p = q; // p = &b と同じ。ここ以降、pはaではなくbを指す(qと同じ)
printf("a: %d, b: %d, *p: %d, *q: %d\n", a, b, *p, *q); // a: 10, b: 3, *p: 3, *q: 3
*p = *q
の例です。これが意味するところは、p
が指している変数の値(=a
)に、q
が指している変数の値(=b
)を代入する
という意味になります。つまり、a=b
です。その後も、p
はa
を指しますし、q
はb
を指します:
int a = 10, b = 3, *p, *q;
p = &a;
q = &b;
printf("a: %d, b: %d, *p: %d, *q: %d\n", a, b, *p, *q); // a: 10, b: 3, *p: 10, *q: 3
*p = *q; // a = b と同じ。pはaを指し、a=3。qはbを指し、b=3
printf("a: %d, b: %d, *p: %d, *q: %d\n", a, b, *p, *q); // a: 3, b: 3, *p: 3, *q: 3
やってみよう(30分)
- 上記を写経してみましょう。ところどころアドレスもプリントするなどして、挙動を確実に理解しましょう。
- ポインタに対するインクリメントなども定義できます。演算子の優先順位から、後置の場合はカッコがいります。以下を試してみましょう。
int a = 10; int *p = &a; ++*p; // *p += 1 という意味。すなわち、a += 1と同じ。 printf("a: %d\n", a); // 11 (*p)++; // かっこがいる printf("a: %d\n", a); // 12
- ポインタを初期化しない状態で値を参照することは未定義です。おかしな結果になりますのでやってはいけません。以下を確認してみましょう。
int *p; printf("%d\n", *p); // 謎の値になる。エラーになり落ちるかも。
クイズ
int a = 10;
int *p = &a;
*&a
&*a
*&p
&*p
答え
*&a
:a
そのもの。まず&a
はa
のアドレスです。それに対して、値を取り出す*
を適用しているので、これはa
そのものになります。&*a
: エラー。*
は通常の変数には適用できないので、*a
はエラーになります。*&p
:p
そのもの。まず&p
はp
のアドレスです。それに対して、値を取り出す*
を適用しているので、これはp
そのものになります。&*p
:a
のアドレス。まず*p
はa
になります。そのアドレスなので。
ここで、*&p
, &*p
, &a
はプリントすると全て同じです。
printf("%p, %p, %p\n", *&p, &*p, &a); // 全部同じ
*&p
はポインタ変数であるp
そのものなので、別のポインタを代入できます。
int b = 0;
*&p = &b; // OK. p = &bと同じ
&*p
は&a
そのものであり変数ではないので、代入できません。
&*p = &b; // ダメ。&a = &bと同じ。エラーになる。
ポインタと関数¶
ここまでポインタの基本を見てきましたが、ポインタは一体何の役に立つのでしょうか? それは多岐にわたると思うのですが、最も身近な例は「関数が複数の値を返したい」ときです。
関数引数¶
Cでは、関数は変数一個しかreturn
することができません。そのため、複数の値を返すときは
引数をポインタにします。
そして、呼び出し元は値を書き込みたい変数のアドレスを関数に渡すことで、その変数を編集してもらいます。
以下に例を見てみましょう。 King, p247
void decompose(double x, long *int_part, double *frac_part) {
*int_part = (long) x; // 小数部分の切り捨て
*frac_part = x - *int_part; // xから整数部分を引くことで小数部分の取得
}
int main() {
long n;
double d;
decompose(3.14, &n, &d);
printf("n: %ld, d: %f\n", n, d); // n: 3, d: 0.140000
}
double x
を受け取って、その整数部分int_part
と小数部分frac_part
を返す関数decompose
を考えましょう。
ポインタを使わなければ、そのように二つの情報をreturn
することはできません。
ここでは、引数にポインタlong *int_part
が登場します。呼び出し側は、値を書き込んでほしい変数long n
を用意します。
そして、そのアドレス&n
を、decompose
に渡します。こうすると、decompose
側では
long *int_part = &n;
n
を編集できるようになります。double *frac_part
についても同様です。
関数の中で
*int_part = ...
n
に値を書き込むことが出来ます。すなわち、以下と同じになります。
n = ...
返り値¶
さて、実は関数が「ポインタを返す」こともできます。以下の例をみてみましょう。King, p251
int *min(int *a, int *b) {
if (*a < *b) {
return a;
} else {
return b;
}
}
int main() {
int num1 = 9;
int num2 = 3;
int *ret;
ret = min(&num1, &num2);
printf("*ret: %d, ret: %p\n", *ret, ret); // *ret: 3, ret: 0x7ffdf9b0c718
printf("&num1: %p, &num2: %p\n", &num1, &num2); // &num1: 0x7ffdf9b0c710, &num2: 0x7ffdf9b0c718
}
min
は、二つの整数のうち小さいほうを返す関数です。
ここでは、返り値としてポインタが設定されています。
min
は、「ポインタa
」の値あるいは「ポインタb
」の値を返します。これは、呼び出し側から見るとnum1
のアドレスあるいはnum2
の
アドレスということです。それが、ret
というポインタに戻ってきます。
結果として、ret
は、num1
かnum2
を指す、ということになります。
この例では、ret
に入っている値(アドレス)は、num2
のアドレスになっていますね。
ちなみにこの例は人工的な例で、ここではポインタを使わずにも同等の関数が作れます。 しかし、次に述べる「ポインタと配列」ではこの表記が重要になってきますので、しっかり理解しておいてください。
やってみよう(20分)
- 上記を写経してみましょう。
クイズ
- 与えられた配列の中から、最大値と最小値を探す次の関数を書いてみましょう。
呼び出すときは次のようにします
void max_min(int a[], int n, int *max, int *min) { ... }
int array[5] = {7, 2, 10, 3, 5}; int max_val, min_val; max_min(array, 5, &max_val, &min_val); printf("max: %d, min: %d\n", max_val, min_val); // max: 10, min: 2
- 上で習った
decompose
を、下のようにポインタを使わずに書くとなぜダメなのか説明してください。実際に実行して確かめてみましょう。void decompose2(double x, long int_part, double frac_part) { int_part = (long) x; // 小数部分の切り捨て frac_part = x - int_part; // xから整数部分を引くことで小数部分の取得 } int main() { long n; double d; decompose2(3.14, n, d); printf("n: %ld, d: %f\n", n, d); }
- 次のように、関数内で作ったローカル変数のアドレスを返すことをしてはいけません。何故でしょうか?また、結果がどうなるか確認してみましょう。
int *f() { int b = 10; return &b; }
答え
- 例えば以下のようになります
King, p250
void max_min(int a[], int n, int *max, int *min) { *max = *min = a[0]; for(int i = 0; i < n; ++i) { if (*max < a[i]) { *max = a[i]; } else if (*min > a[i]) { *min = a[i]; } } }
main
側から関数に引数を渡すときは「値のコピー」になります。なので、n
を引数に渡すとき、渡すのはそのコピーです。関数の内側でint_part
をどう編集しようが、元のn
には影響しません。そのため、関数は結果をmain
側に返すことが出来ません。実は、この「関数に情報を渡すときはコピーしかできない」ということは、基本的なルールです。なので、ポインタ表記にしてアドレスを渡すという処理も、「アドレスという数値をコピー」しているにすぎません。- 関数の内側で作った変数は「関数のローカル変数」であり、関数から出たときには消滅し、二度とアクセスできません。なので、そのような「消滅したもののアドレス」は無効な値です。それを
main
側に戻して 再利用してはいけません。未定義の動作になります。以下のコードで検証してみましょう。ここでは、#include <stdio.h> int *f() { int b = 10; return &b; } int main() { int *a = f(); printf("*a: %d\n", *a); // Segmentation fault (core dumped) }
a
には関数のローカル変数のアドレスを指すというやってはいけないことをしてしまっているので、Segmentation fault
というエラーになりました。ちなみに、ポインタで不正なことをするとこのSegmentation fault
というエラーがよく出てきます。このエラーはやっかいですので注意しましょう。
ポインタと配列¶
次にポインタと配列について勉強しましょう。Cでは、ポインタと配列は極めて強い関係があります。 ポインタは、その値として変数のアドレスを持つことをここまで勉強してきました。全く同様に、ポインタは、配列中の要素を指すこともできます。 次の例を見てみましょう。
int a[3] = {12, 3, 5};
int *p = &a[0]; // &(a[0])のこと
int
型配列a
の最初の要素(a[0]
)のアドレスを、ポインタp
に代入しています。
これにより、p
はa[0]
を指します。それを可視化したものが次になります。
これまで同様、ポインタにアスタリスクを発動すると、指している変数の値(配列の要素)を取得することができます。
printf("%d\n", *p); // 12
*p = 2; // aは{2, 3, 5}になった
p = &a[1]
とすれば、p
はa[1]
を指すことができます。
ポインタに対する操作¶
さて、ポインタには次のような操作が可能です。
- ポインタに対し、同じ型の他のポインタの代入(これは既に見ました)
- ポインタに対し、整数の加算と減算
- 同じ配列を指す二つのポインタの引算と比較
NULL
の代入やNULL
との比較
これを見るために、次の例を考えましょう。
ここではまず配列とポインタ2つを作ります。
int a[10], *p, *q;
p
はa
の先頭を指します。
p = &a[0];
p + 2
という表記は、「配列中でpが指している要素から、2つ進んだ要素」を意味します。
そして、それを別のポインタq
に代入しています。
これにより、q
はa[2]
を指すようになります。
これを確認してみましょう。
printf("&a[0]: %p, p: %p\n", &a[0], p); // &a[0]: 0x7ffe0577ff50, p: 0x7ffe0577ff50
printf("&a[2]: %p, q: %p\n", &a[2], q); // &a[2]: 0x7ffe0577ff58, q: 0x7ffe0577ff58
p
はa
の先頭を指したままですね。
q
はp
から8バイト(=4バイトのintが2つ)分だけ
進んだ位置、すなわち&a[2]
を指していることがわかりますね。
ちなみに、もし配列の要素数を超えた先にアクセスしてしまうと当然エラーになりますので、注意しましょう。
たとえばq = p + 10
とすると、*q
は未定義の動作になります。
また、整数加算と代入ができるので、次のような表記もできます。これにより、p
はa[4]
を指すようになります。
p += 4;
コラム
p + 2
と書いたからといって、アドレスに2を足したものになるわけではないことに注意してください。
例えば上の例では
p
:0x7ffe0577ff50
p + 2
:0x7ffe0577ff58
(これは0x7ffe0577ff50 + 2 = 0x7ffe0577ff52
ではない)
ここでは、2ではなくsizeof(int) * 2
が追加されています。
より詳しく言うと、ある型T
の要素を指すポインタp
, q
について、
q = p + 2
と書いたとき、q
にはpのアドレス + sizeof(T) * 2
の分だけアドレスが加算されたものが代入されます。
ここではint
なので4バイトでしたが、double
では8バイトです。ここは自動的に決定されますので、
ようはポインタに整数を足すとその分だけ進んだ配列要素に辿り着くと覚えておいて下さい。
さて、次の例も見てみましょう。 ここでは、
q = &a[3];
q
はa
の4つ目の要素を指すようになります。
また、代入と加算が出来るため、インクリメントやデクリメントも行うことができます。
p++;
p
はa[0]
ではなくa[1]
を指すようになります。
最後に、ポインタp
, q
について、その差を計算することができます。
int i = p - q; // -2
int j = q - p; // 2
p
とq
は同じ配列を指している必要があります。
違う配列を指している場合、結果は未定義です。
また、同じ配列を指しているポインタ同士に対して、比較や一致の判定を行うことができます。 例えば、この場合は以下のようになります
printf("p < q: %d\n", p < q); // p < q: 1
printf("p == q: %d\n", p == q); // p == q: 0
q
はp
より後ろを指しているので、p < q
は真です。また、この二つは一致しないので、p == q
は偽です。
コラム
ポインタには(1) アドレス か (2) 他のポインタ しか代入することはできませんが、唯一の例外として、
NULL
を代入することができます。
int *p;
p = NULL;
printf("p: %p\n", p); // p: (nil)
NULL
を代入するということは、処理の途中で、ポインタがどこも指さないことを明示的に示す、ということです。
NULL
は実際はただの整数の0です。
NULL
が入っているポインタに*p
をしてはいけません。どこも指していないので動作は未定義です。
p
がNULL
かどうかの判定はif (p == NULL) { ... }
のように書けますが、これは短く if (!p) { ... }
とすることもできます。
ソフト1の範囲ではNULL
は出てこないのですが、ソフト2でmalloc
という概念を習うときに出てきます。
ポインタで配列を走査¶
さて、それでは上記を踏まえてポインタで配列を走査する方式を見てみましょう。King, p261
#include <stdio.h>
#define N 5
int main () {
int a[N] = {2, 6, 1, 4, 7};
int sum1, sum2;
sum1 = sum2 = 0;
// 添え字バージョン
for (int i = 0; i < N; ++i) {
sum1 += a[i];
}
printf("sum1: %d\n", sum1); // 20
// ポインタバージョン
int *p;
for (p = &a[0]; p < &a[N]; ++p) {
sum2 += *p;
}
printf("sum2: %d\n", sum2); // 20
}
#define N 5
を解説しましょう。これは、ソースコード中でN
と書いた部分を、コンパイルする段階で全て5
に書き換えてくれ、という意味です。
定数を定義しているようなものです。コード中にN
がたくさんでてくるときに、全て5
と直接書いていては、あとでそれを10
に変更しようと思うと
全ての部分を変更しなければなりません。よって、上の例のようにN
と書いておいて、それにdefine
で値を設定する、というようなことはよくやられます。
さて、添え字バージョンを見てみましょう。これは普通に添え字i
を使って配列を走査し、合計値を計算しています。
これをポインタを使って書き換えたものがポインタバージョンです。ここではポインタp
を作り、
それに配列の先頭アドレスをまず設定します。これが配列の「最後の次」のアドレスに一致しない間、ループを続けます。
ループの間、*p
の表記で配列要素にアクセスします。また、配列の次の要素を指すために、インクリメントを行い自身を移動させます。
このようにして、ポインタを使うことで、配列を走査することができます。
また、次のように、ポインタの宣言はfor
に含めることもできます。
for (int *p = &a[0]; p < &a[N]; ++p) { ... }
コラム
上記で、アレッと思いませんでしたか?というのも、配列はa[0]
からa[N-1]
までなので、
&a[N]
という表記はいいのか?という疑問がわいてきます。実は、ここはOKです。というのも、
a[N]
のように「値」にアクセスすると、配列外アクセスでダメなのですが、&a[N]
という「アドレス」は、
完全に定義される(&a[N-1]
にsizeof(int)
を足したもの)からです。
ポインタの演算では、このように、配列の境界の直後の要素のアドレスとの比較は境界条件としてよく使われます。
配列名とポインタ¶
また、先週に少しお話した通り、配列名だけを書くとそれはその配列の先頭要素のアドレスを意味するというルールがあります。 よって、以下の表記は、配列の先頭アドレスの代入と同じになります。
p = a; // p = &a[0] と同じ
&a[3]
のような配列要素のアドレスは、a + 3
とも書くことができます。
これを踏まえて以下を見てみましょう。
p = a
, およびq = a + 3
という表記で、配列中の要素を指定できています。
そして、次の3つは同じものを指します。
int v1 = a[3];
int v2 = *(a + 3);
int v3 = *q;
ここからわかるように、Cでは配列(a
)とポインタ(p
)はかなり似た概念になっています。
注意として、ポインタには別のポインタを代入できますが、配列のアドレスには当然できません。すなわち、以下のようになります。
int a[] = {1, 2, 3, 4, 5}, *p;
p = a;
int *r = NULL;
p = r; // 出来る(ポインタにポインタを代入)
a = r; // 出来ない(aは固定されている配列アドレスなので、別のものを代入できない)
sizeof
の値も違うことを思い出しましょう。
int a[] = {1, 2, 3, 4, 5}, *p;
p = a;
printf("sizeof(p): %lu\n", sizeof(p)); // sizeof(p): 8
printf("sizeof(a): %lu\n", sizeof(a)); // sizeof(a): 20
sizeof
を適用すると、ポインタ変数1つ分の大きさとして8バイト(64ビットマシンの場合)が出力されます。
一方で、配列にsizeof
を適用すると、4バイトが要素数分、すなわち4 * 5 = 20
バイトが出力されます。
コラム
配列a[]
の要素にアクセスする際、通常の添え字アクセス(a[2]
)以外にも、まるでポインタのような表記(*(a + 2)
)が可能でした。
これは実は逆もしかりで、ポインタに対して添え字でアクセスすることもできます。
int a[] = {1, 2, 3};
int *p = a;
printf("%d, %d\n", *(p + 2), p[2]); // 3, 3
関数に配列を渡すとは¶
さて、実は、「関数に配列を渡す」とは実は「関数にポインタを渡す」ことなのです。
int sum_array (int a[], int n) {
int sum = 0;
for (int i = 0; i < n; ++i) {
sum += a[i];
}
return sum;
}
int sum_array (int *a, int n) { ... }
int a[]
といった配列表記は、int *a
といったポインタ表記と全く同じ意味になります。
よって、「関数に配列を渡す」ときは、配列全てをコピーするのではなく、関数の先頭アドレス一個だけをコピーします。 これは速度・メモリの消費が抑制されます。すなわち、もし超巨大な配列を関数に全てコピーしていたら、 それだけで時間がかかってしまいます。アドレス一個だけならば、配列がどんなに大きくても高速に扱うことができます。
また、ポインタである以上、実は関数内から配列要素を変更することができます。次の例を見てみましょう。
void set_one(int a[], int n) {
for(int i = 0; i < n; ++i) {
a[i] = 1;
}
}
int main() {
int a[] = {2, 3, 4};
printf("a = {%d, %d, %d}\n", a[0], a[1], a[2]); // a = {2, 3, 4}
set_one(a, 3);
printf("a = {%d, %d, %d}\n", a[0], a[1], a[2]); // a = {1, 1, 1}
}
関数引数を配列形式にするかポインタ形式にするかは状況によります。配列を入力したいということを明示的に表せ、 かつわかりやすいのが配列形式です。一方、ポインタであることを強調するためにポインタ形式にすることもあります。
コラム
かなりややこしいのですが、
- 変数宣言時の
int a[]
とint *a
は意味が違う。配列か。ポインタか。 - 関数引数の
int a[]
とint *a
は同じで、どちらもint *a
というルールになっています。これはsizeof
で確認できます。
// 再掲
int a[] = {1, 2, 3, 4, 5}, *p;
p = a;
printf("sizeof(p): %lu\n", sizeof(p)); // sizeof(p): 8
printf("sizeof(a): %lu\n", sizeof(a)); // sizeof(a): 20
void g1(int a[]) {
printf("sizeof(a) in g1(int a[]): %lu\n", sizeof(a)); // sizeof(a) in g1(int a[]): 8
}
void g2(int *a) {
printf("sizeof(a) in g2(int *a): %lu\n", sizeof(a)); // sizeof(a) in g2(int *a): 8
}
int main() {
int a[] = {1, 2, 3, 4, 5};
g1(a);
g2(a);
}
a[]
でも*a
でも、ポインタ分の8バイトになります。
このことからも、関数引数の配列表記はポインタだということがわかります。
やってみよう(30分)
- 上記を写経してみましょう。
-
上記の「配列を走査する例」は次のようにも書けます。これを眺めて、挙動を理解しましょう。
int a[N] = {2, 6, 1, 4, 7}; for (int *p = a; p < a + N; ++p){ sum += *p; }
-
Cの有名な謎挙動として、配列
a
に対し、a[2]
のことを2[a]
とも書ける、というものがあります。次を実行してみましょう。ここで、コンパイラはint a[] = {1, 2, 3}; printf("%d, %d, %d, %d\n", a[2], *(a + 2), *(2 + a), 2[a]); // 3, 3, 3, 3
2[a]
という記述を、*(2 + a)
だと思って扱います。足し算は入れ替えてもいいので、これは*(a + 2)
に等しいです。なので、それはa[2]
に等しい、というわけです。 もちろん、このようなコードを実際に書いてはいけません。
クイズ
引数の二つの変数を入れ替えるswap
関数を次のように考えます。
void swap (double a, double b) {
double tmp = a;
a = b;
b = tmp;
}
a
の要素を昇順にしたいです。
double a[] = {1.0, 5.0, 3.0, 4.0, 2.0, 6.0};
- 上の
swap
関数を用いてswap(a[1], a[4])
としてもうまくいきません。なぜか説明したうえで、うまくいくように書き換えてください。 swap
関数を用いてa
を昇順にしてください。その際、入力の形式を二通り書いてください。
答え
swap
関数は呼び出し元の値を変更するため、ポインタにしなければならない。例えば以下:
void swap (double *a, double *b) {
double tmp = *a;
*a = *b;
*b = tmp;
}
a
を並べ替えるには次の二通りがある:
swap(a + 1, a + 4);
swap(&a[1], &a[4]);
ポインタと文字列¶
文字列はchar
の配列であることは既に勉強しました。よって、文字列に対する操作とポインタにも密接なつながりがあります。
char s[] = "hoge";
char *p = s;
printf("%c %c %c %c \n", s[2], *(s + 2), *(p + 2), p[2]); // g g g g
コラム
実際にアドレスを見てみると、p + 1
としたときに、sizeof(char)
すなわち1
だけ値が増えていることを見ておきましょう。p
をchar
への配列として、
printf("p: %p, p+1: %p\n", p, p + 1); // p: 0x7ffc75cc2413, p+1: 0x7ffc75cc2414
int
のポインタのときは、ここはsizeof(int)
すなわち4
だけ増えていましたね。
このあたりは、コンパイラが自動的によしなにしてくれますので、ようは、型がなんであれ、ポインタを1増やすと配列中の次の要素に進む、と覚えておきましょう。
文字列を扱う関数の例¶
それではポインタを使って文字列を扱う関数の例をいくつか見てみましょう。 King, p296
int strlen1(char *s) {
int n = 0;
for(; *s != '\0'; s++) {
n++;
}
return n;
}
int main() {
char s[] = "abc";
printf("%d\n", strlen1(s)); // 3
}
strlen1
関数は、文字列を受け取り、その長さを返すものです。最後にNULL文字\0
が来るまでチェックを行い、
文字をカウントします。これは簡単ですね。次に、これと同じような内容を、別の書き方で書いてみます。
int strlen2(const char *s) {
int n = 0;
for (; *s; s++) {
n++;
}
return n;
}
const
という記号を足しました。これは、sが指す値を変更できないという条件をs
に課しています。
strlen
関数は文字列s
にアクセスする必要がありますが、その内容を変更する必要はありません。
ですが、char *s
というポインタを渡すやり方だと、関数の内側からs
を変更できてしまいます。
これは、バグの温床になりますし、関数を使う側からすると気持ち悪いです。
そのような際に、const char *s
という表記をすると、s
が指す値すなわち*s
を変更することが出来なくなります。
このような表記を覚えておきましょう。
次に、strlen1
であった*s != '\0'
が単に*s
に変更されています。
これは、NULL文字'\0'
は実際はただの0という数値なので、0と違うということはたんに*s
と書けるからです。
さらに次のバージョンを見てみましょう。
int strlen3(const char *s) {
const char *p = s;
while (*s) {
s++;
}
return s - p;
}
s
をconst char *p
で受け取っています。
ここでconst
が必要なことに注意してください(でなければ、p
を経由してs
の指しているものを変更できてしまいます)。
そして、strlen2
であったforループは上のようにwhileで書き直せます。
最後に、ループで進んだ回数が文字列長そのものなので、それはポインタの差分s - p
で表現できます。
コラム
ちなみに、const int *p
という書き方は、p
そのものではなく、p
が指している値を変更できないという意味になります。すなわち、*p
を変更できないということです。
また、*(p + 3)
のように、p
を経由した配列先の他の要素の変更も出来ません。一方で、int * const p
という書き方をすると、p
そのものは変更できなくなりますが、*p
は変更できます King, p254
。
まとめると、以下です。
void f1(const int *p) {
// !!!! 出来ない !!!!
*p = 3; // ポイント先の値の変更
*(p + 2) = 5; // ポインタ経由での配列要素の変更
// OK
int a;
p = &a; // ポインタそのものの変更
}
void f2(int * const p) {
// OK
*p = 3; // ポイント先の値の変更
*(p + 2) = 5; // ポインタ経由での配列要素の変更
// !!!! 出来ない !!!!
int a;
p = &a; // ポインタそのものの変更
}
void f3(const int * const p) {
// !!!! 出来ない !!!!
*p = 3; // ポイント先の値の変更
*(p + 2) = 5; // ポインタ経由での配列要素の変更
// !!!! 出来ない !!!!
int a;
p = &a; // ポインタそのものの変更
}
文字列の初期化¶
さて、文字列の初期化について少し見ておきましょう。以下の二通りの初期化はどちらもOKです
char amsg[] = "abc";
char *pmsg = "abc";
printf("%s %s\n", amsg, pmsg); // abc abc
char amsg[]
: char型の変数を4個実際に確保。その中に'a'
,'b'
,'c'
,'\0'
を入れる。これらは普通の配列なので、自由にアクセス・変更できる。この表記は、一般の配列の初期化と同様。char amsg[] = {'a', 'b', 'c', '\0'}
の省略表記だと思ってOKchar *pmsg
: char型ポインタを1個だけ確保。プログラムのどこかで文字列リテラル"abcd"
が確保される。そのリテラルのアドレスが入る。リテラルなので、アクセスは出来るが変更は出来ない。この表記は文字列専用の特殊なもの。
以下の例が違いを表しています。まず、pmsg
はポインタなのでポインタやNULL
を代入できますが、amsg
は配列なのでそのようなことはできません。
amsg = NULL; // ダメ
pmsg = NULL; // OK
また、amsg
は配列なので要素を変更出来ますが、pmsg
が指しているものはリテラルなので変更できません。
amsg[0] = 'A'; // OK
pmsg[0] = 'A'; // ダメ
ここで「リテラルは変更できない」というのは、例えば整数リテラル13
を考えると、この13
そのものは変更できないということです。
つまり、13 = 5;
のようなことはできません。同様に、"abc" = "xyz";
もできません。
同様に、「"abc"[0] = 'A';
として"abc"
を"Abc"
にする」こともできません。
コラム
なんで文字列リテラルを変更できないんだろう?と思うかもしれません。例えば以下の例を考えます(以下の例は環境依存ですので、プラットフォームが違うと話が変わるかもしれません。Google Cloud Shell Editorにおけるgccでは下記のようになります)。
char *p = "abc";
char *q = "abc";
p
, q
を作った場合、それぞれに対し"abc"
という領域がどこかに作られてそのアドレスが割り当てられていると考えるかもしれません。
ですが、コンパイラが賢くやってくれる場合、"abc"
という領域の確保は一度だけ行われ、p
とq
はその同じ領域を指します(この機能のおかげで、巨大なリテラルを何度も作るような場合も、無駄なメモリ消費が起きません)。
printf("%p %p\n", p, q); // 0x55e91a33b004 0x55e91a33b004 <- 同じ!
p[0] = 'x'
としてp
が指す内容を"xbc"
とすることが出来てしまう場合、
全然別の変数であるq
の中身も"xbc"
になってしまい、話がおかしくなりますね。このように、文字列リテラルは編集不可能になっています。 King, p305
.
やってみよう(30分)
- 上記を写経してみましょう。
クイズ
- 文字列をコピーする関数
my_strcpy
を考えましょうK&R, p128
。ここでは、s1がコピー先で、s2がコピー元です。以下に添え字を使ったバージョンを書きました。while
の条件式が何をしているか、考えてみましょう。 これを編集して、添え字iを使わずにポインタのインクリメントのみで計算できるよう、書き直してみましょう。#include <stdio.h> void my_strcpy(char *s1, const char *s2) { int i = 0; while ((s1[i] = s2[i]) != '\0') { ++i; } } int main() { char str1[] = "aaaaaaaaaa"; char str2[] = "hoge"; printf("str1: %s\n", str1); // str1: aaaaaaaaaa my_strcpy(str1, str2); printf("str1: %s\n", str1); // str1: hoge // ここでは、str1は以下のようになっています: // {'h', 'o', 'g', 'e', '\0', 'a', 'a', 'a', 'a', 'a', '\0'} }
答え
-
whileの条件式の説明は以下です。
s1[i] = s2[i]
で、コピー元のs2
のi
番目の要素を、コピー先のs1[i]
に代入しています。つまり、どんどん上書きしています。(s1[i] = s2[i]) != '\0'
ここでは、「代入式の値は、代入した値そのもの」であることを思い出しましょう。つまり、(s1[i] = s2[i])
は、s2[i]
そのものになります。s2[i]
が終端文字の'\0'
であるまで、ループを更新する、という意味になりますね。
ポインタで書き直すと、例えば以下です
これはさらに下のようにも書けます。void my_strcpy2(char *s1, const char *s2) { while ((*s1 = *s2) != 0) { s1++; s2++; } }
void my_strcpy3(char *s1, const char *s2) { while (*s1++ = *s2++) { ; } }
宿題¶
それでは今週の宿題です。
- 締切は次の授業の前日の深夜23:59までです(今回の場合、2024/10/30, 23:59)
- 宿題リンクをslackで配付します。このリンクは公開しないでください。
- 便利な関数などは何も使わずに、全て自作してください。
week4_1¶
2つのアルゴリズム、max_window
とroundrobin
を実装しましょう。以下、出来るところまでやってみてください。
タスクA¶
まずタスクAです。以下のように、./a.out
のあとにa
を入力し、その後に小数をいくつか入力します。全てスペース区切りです。ここで小数というのは負の数も含みます。
$ ./a.out a -0.2 1.2 3.4 -2.1 7.1 -5.4 2.3
a
はタスクAを実行することを意味します。ソースコード中のmain
関数の中で// タスクAのとき
となっている部分に進みます。
そして、int N
に入力された小数の個数が入ります。また、
double vals[N]
の中にその小数が入ります。たとえば上記の場合ですと
int N = 7;
double vals[N] = {1.2, 3.4, -2.1, 1.1, -5.4, 2.3, 2.6};
N
は20個以下を想定しOKです。小数は極端に絶対値が大きい値はとらなくてよいと仮定して大丈夫です。
ここで、以下の記載があります。
int pos1;
int pos2;
double max = max_window(vals, N, &pos1, &pos2);
printf("pos1 = %d, pos2 = %d, max = %.2f\n", pos1, pos2, max);
max_window
関数は、配列vals
の中で、ある連続する部分配列を探します。その部分配列は、要素の合計値が最大になるものです。そしてその合計値を返します。
その際、部分配列の先頭と最後の添え字もpos1
, pos2
として返して下さい。
たとえば上記の場合ですと、pos1 = 1
, pos2 = 4
, max = 9.60
となります。
すなわち、vals[1] + vals[2] + vals[3] + vals[4]
が9.60
となり、これがとりうる連続部分配列の中で合計値最大になっています。
これを実現するために、ソースコード中の
double max_window(const double *arr, int N, int *pos1, int *pos2) {
// この中を編集
return 0;
}
関数は好きに追加していいですが、今回はmain関数の中身は変更しないでください。
自動採点は以下の2つです。まず、正の数が来た場合は当然ながら配列そのものが答えになります。
入力
$ gcc ./a.out a 1.1 2.2 3.3
pos1 = 0, pos2 = 2, max = 6.60
次に、負の数を含んでいた場合、どこかの部分配列が最大の値をとります。
入力
$ gcc ./a.out a -0.2 1.2 3.4 -2.1 7.1 -5.4 2.3
pos1 = 1, pos2 = 4, max = 9.60
ちなみに、最大値をとる部分配列が2つ得られるような入力は想定しなくて大丈夫です。
タスクB¶
次にタスクBです。以下のように、./a.out
のあとにb
を入力し、その後に3つの文字列を入力します。これらの文字列は全て1字以上8字以下を仮定して大丈夫です。
$ ./a.out b abc def ghi
これにより、main
関数の中で// タスクBのとき
となっている部分に進みます。
そして、以下のように3つのポインタにそれぞれの文字列が入ります。すなわち、
char *s1 = "abc";
char *s2 = "def";
char *s3 = "ghi";
そして、以下の記載があります。
char s[] = "111111111111111111111111111111";
roundrobin(s1, s2, s3, s);
printf("%s\n", s);
ここでは、結果記入用のchar
型配列s
が準備されています。これは大量の1
で初期化されています。
そして、roundrobin
関数を呼びます。この関数は以下の挙動をとります。
s1
, s2
, s3
を順番に見ていって、先頭から一字ずつ抜き出してきて、それを組み合わせます。これをs
に書きだします。上記の場合ですと、adgbehcfi
になります。
ちなみに抜き出せない場合(文字列が短くて全部処理してしまった場合)はスキップします。
このために、以下の関数内を編集してください。
void roundrobin(const char *s1, const char *s2, const char *s3, char *s) {
// この中を編集
}
自動採点は以下の2つです。まず上記の例です。
入力
$ ./a.out b abc def ghi
adgbehcfi
次に、各文字列の長さが違う場合を考えましょう。抜き出せない場合はスキップするとして、以下のようになります。
入力
$ ./a.out b abc de fghi
adfbegchi
答えの例¶
答え
week4_1
#include <stdio.h>
#include <stdlib.h>
// 上記以外は何もincludeしないでください
// 関数は自由に追加していいです。
double max_window(const double *arr, int N, int *pos1, int *pos2) {
double max = -1000000000000;
int max_pos1 = 0;
int max_pos2 = 0;
// 単純に全組み合わせを調べる
for (int n1 = 0; n1 < N; ++n1) {
for (int n2 = n1; n2 < N; ++n2) {
double sum = 0.0;
for (int n = n1; n <= n2; ++n) {
sum += arr[n];
}
// ループ周回中で最大/最小値を維持・更新するための基本パターン
if (sum > max) {
max = sum;
max_pos1 = n1;
max_pos2 = n2;
}
}
}
// 位置情報をポインタ経由でmain側に伝える
*pos1 = max_pos1;
*pos2 = max_pos2;
return max;
}
void roundrobin(const char *s1, const char *s2, const char *s3, char *s) {
int n, n1, n2, n3;
n = n1 = n2 = n3 = 0;
while (1) {
// 全て終端に達した場合終了
if (s1[n1] == '\0' && s2[n2] == '\0' && s3[n3] == '\0') {
break;
}
// 終端に達してい場合、コピー
if (s1[n1] != '\0') {
s[n] = s1[n1];
n1++;
n++;
}
if (s2[n2] != '\0') {
s[n] = s2[n2];
n2++;
n++;
}
if (s3[n3] != '\0') {
s[n] = s3[n3];
n3++;
n++;
}
}
// 最後のNULL文字を忘れない
s[n] = '\0';
}
int main(int argc, char *argv[]) {
// ==== main関数は更新しない ====
if (argc < 2) {
printf("Error. Too few arguments.\n");
return 0;
}
if (argv[1][0] == 'a') {
// タスクAのとき
// ===== 引数の処理 ====
int N = argc - 2;
double vals[N];
for (int n = 0; n < N; ++n) {
vals[n] = atof(argv[n + 2]);
}
// $ ./a.out a -0.2 1.2 3.4 -2.1 7.1 -5.4 2.3 のとき、
// vals = {-0.2 1.2, 3.4, -2.1, 7.1, -5.4, 2.3} および N = 7 になっている
int pos1;
int pos2;
double max = max_window(vals, N, &pos1, &pos2);
printf("pos1 = %d, pos2 = %d, max = %.2f\n", pos1, pos2, max);
} else if (argv[1][0] == 'b') {
// タスクBのとき
// ===== 引数の処理 ====
char *s1 = argv[2];
char *s2 = argv[3];
char *s3 = argv[4];
// $ ./a.out b abc de fghi のとき、
// s1 = "abc", s2 = "de", s3 = "fghi" になっている
char s[] = "111111111111111111111111111111";
roundrobin(s1, s2, s3, s);
printf("%s\n", s);
} else {
printf("Error. Put 'a' or 'b' after './a.out'.\n");
}
return 0;
}