コンテンツにスキップ

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
そして、ポインタ自身も単なる変数(メモリ上に確保された領域)なので、ポインタpそのもののアドレスも当然存在し、それは上記とは全く別のものになります。
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*」とする、という形です。実際、上記のようにアスタリスクを左に寄せた表記でもOKです。 この表記を使う人もいます。

しかしこの表記には少し問題があります。「int*」を型だと思って、ポインタを三つ作ろうと思い以下のようにすると間違いです。

int* p1, p2, p3;
これはint *p1, int p2, int p3と解釈され、p2p3は普通のintになってしまいます。 よって、表記には十分注意してください。 たとえば次の表記は、
int a, b, *p;
abが普通のintで、pがポインタになります。 ちなみに、変数がポインタかどうか調べるにはsizeofを使えばいいです。64bit環境では、sizeof(a)は4になり、sizeof(p)は8になります。

コラム

c言語のドはまりポイントとして、ポインタ宣言のときにアスタリスク(*)を使い、ポインタが指し示す変数にアクセスするときもアスタリスクを使うという点にあります。 この二つは全く別の操作なのですが、なぜかどちらにもアスタリスクが割り振られています。 すなわち、

int a = 10;
int *p;  // (1) ポインタを作るときはアスタリスクをつけるというルール
p = &a;
printf("%d\n", *p);  // (2) ポインタの特殊能力:アスタリスクをつけると指している変数の値にアクセス
上記の(1)と(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です。その後も、paを指しますし、qbを指します:
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;
としたとき、以下の4つは何を指しますか?

  • *&a
  • &*a
  • *&p
  • &*p
答え
  • *&a: aそのもの。まず&aaのアドレスです。それに対して、値を取り出す*を適用しているので、これはaそのものになります。
  • &*a: エラー。*は通常の変数には適用できないので、*aはエラーになります。
  • *&p: pそのもの。まず&ppのアドレスです。それに対して、値を取り出す*を適用しているので、これはpそのものになります。
  • &*p: aのアドレス。まず*paになります。そのアドレスなので。

ここで、*&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は、num1num2を指す、ということになります。 この例では、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に代入しています。 これにより、pa[0]を指します。それを可視化したものが次になります。 これまで同様、ポインタにアスタリスクを発動すると、指している変数の値(配列の要素)を取得することができます。
printf("%d\n", *p); // 12
また、ポインタ経由で配列の要素の変更も出来ます。
*p = 2; // aは{2, 3, 5}になった
全く同様に、p = &a[1]とすれば、pa[1]を指すことができます。

ポインタに対する操作

さて、ポインタには次のような操作が可能です。

  • ポインタに対し、同じ型の他のポインタの代入(これは既に見ました)
  • ポインタに対し、整数の加算と減算
  • 同じ配列を指す二つのポインタの引算と比較
  • NULLの代入やNULLとの比較

これを見るために、次の例を考えましょう。

ここではまず配列とポインタ2つを作ります。

int a[10], *p, *q;
先ほど習った通り、以下の表記でpaの先頭を指します。
p = &a[0];
ここで、p + 2という表記は、「配列中でpが指している要素から、2つ進んだ要素」を意味します。 そして、それを別のポインタqに代入しています。 これにより、qa[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
ここでわかる通り、paの先頭を指したままですね。 qpから8バイト(=4バイトのintが2つ)分だけ 進んだ位置、すなわち&a[2]を指していることがわかりますね。 ちなみに、もし配列の要素数を超えた先にアクセスしてしまうと当然エラーになりますので、注意しましょう。 たとえばq = p + 10とすると、*qは未定義の動作になります。

また、整数加算と代入ができるので、次のような表記もできます。これにより、pa[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];
という表記で、qaの4つ目の要素を指すようになります。 また、代入と加算が出来るため、インクリメントやデクリメントも行うことができます。
p++;
という表記で、pa[0]ではなくa[1]を指すようになります。 最後に、ポインタp, qについて、その差を計算することができます。
int i = p - q; // -2
int j = q - p; // 2
その結果は、ポインタで指している要素間の要素数の差になります。 ちなみに、注意として、pqは同じ配列を指している必要があります。 違う配列を指している場合、結果は未定義です。

また、同じ配列を指しているポインタ同士に対して、比較や一致の判定を行うことができます。 例えば、この場合は以下のようになります

printf("p < q: %d\n", p < q);   // p < q: 1
printf("p == q: %d\n", p == q); // p == q: 0
ここでは、qpより後ろを指しているので、p < qは真です。また、この二つは一致しないので、p == qは偽です。

コラム

ポインタには(1) アドレス か (2) 他のポインタ しか代入することはできませんが、唯一の例外として、 NULLを代入することができます。

int *p;
p = NULL;
printf("p: %p\n", p);  // p: (nil)
ポインタにNULLを代入するということは、処理の途中で、ポインタがどこも指さないことを明示的に示す、ということです。 NULLは実際はただの整数の0です。 NULLが入っているポインタに*pをしてはいけません。どこも指していないので動作は未定義です。 pNULLかどうかの判定は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
上で述べた通り、配列のsizeofとポインタのsizeofはちがいます。
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だけ値が増えていることを見ておきましょう。pcharへの配列として、

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;
}
ここではまず、sconst 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'}の省略表記だと思ってOK
  • char *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"という領域の確保は一度だけ行われ、pqはその同じ領域を指します(この機能のおかげで、巨大なリテラルを何度も作るような場合も、無駄なメモリ消費が起きません)。
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]で、コピー元のs2i番目の要素を、コピー先の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_windowroundrobinを実装しましょう。以下、出来るところまでやってみてください。

タスク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) {
    // この中を編集
}
今回も、関数は好きに追加していいですが、main関数の中身は変更しないでください。

自動採点は以下の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;
}