第27回
データ構造(6)~ポインタを使った引数の受け渡し

constキーワード

前回(第26回)では、引数の文字列にconstキーワードを使った関数をいくつか紹介しました。constキーワードの働きとその意味を説明しておきます。

書き換えられないことを示す

constキーワードは、ポインタを使った引数に対して『その示す先の値を書き換えることができない』ことを明示します。

通常、引数をポインタを介してアドレスとして受け取った場合、関数の内部ではそのアドレスの示す先の値を書き換えることができます。先述した、いわゆる参照渡しとなるのですから、これは当たり前です。

しかし、場合によっては引数の示す先にある値を勝手に書き換えては困る場合もあります。そのような場合にconstを冠し、ポインタの示す先の値を書き換えてはいけないことを明示しておくのです。

送り側の文字列は書き換えない

例えばstrcpy関数では第1引数、strcat関数では第2引数にconstキーワードを冠しています(リスト5、リスト6)。

リスト5のstrcpy関数では、第1引数の示す文字列を第2引数の示す場所にコピーします。このとき第2引数の示す先の値は書き換えられなければなりませんが、コピー元である第1引数を書き換えてはいけません。

もちろん、関数内部でそのようなコードを記述しなければよいのですが、プログラミング時のうっかりミスで不用意に書き換えてしまうことがあるかもしれません。それを防ぐため、constキーワードを使って関数の定義時に『第1引数は書き換えてはいけない』ことを明示します。

リスト6のstrcat関数では、第1引数の示す文字列に第2引数の示す文字列を連結します。書き換えられてはいけないのは連結元である第2引数なので、第2引数にconstキーワードを付けています。

リスト5:strcpy関数(文字列のコピー)
char * strcpy(const char *s, char *d)
{
  while ((*d++ = *s++) != '\0') {
  /* 条件式の中でコピーとポインタのインクリメントを行う */
  }
  *d = '\0';
  return (d);
}

リスト6:strcat関数(文字列の連結)
char * strcat(char *s1, const char *s2)
{
  char *p;   /* 作業用のポインタ */
  p = s1;    /* s1の値を受け取る */

  /* p(s1)の末尾までポインタを進める */
  while (*p != '\0') {
    p++;
  }
  /* p(s1)の末尾にs2をコピー */
  while (*s2 != '\0') {
    *p++ = *s2++;
  }
  *p = '\0';
  return (s1);
}

*

関数に引数を渡す2とおりの方法とその違いを紹介しました。ポインタを介することで、変数の持つ値ではなく変数の場所(アドレス)を関数に渡して、呼び出し側の変数の値を書き換えることができます。

数値型の変数では、値を入れ替えるswap関数のような場合を除けば、引数にポインタを用いる機会はそれほどありませんが、文字列など配列を扱う処理で威力を発揮します。

また、複数の変数を1つにまとめて扱える構造体では、ポインタを介することで柔軟な処理を作れます。構造体とポインタについては、回を追って紹介しましょう。

今回紹介したソースの動作をまとめて試すためのプログラムをリスト7に掲げておきます。コンパイルして実行すると、以下のように表示されます(---- 以降は結果の説明なので表示されません)。

実行結果
----------------------------------------------------------------------
Test of isquare1 : ans = 100 -------------- 10の2乗
Test of isquare2 : ans = 10000 ------------ 100の2乗
Test of iswap1 : x = 10, y = 100 --------- 値は入れ替わらない
Test of iswap2 : x = 100, y = 10 --------- 値が入れ替わっている
----------------------------------------------------------------------

リスト7:関数の動作を試すプログラム(ex27.cEX27.EXE
#include <stdio.h>

/* リスト1:引数の値を2乗して返す関数 */
int ipower1(int n)
{
  return(n * n);
}

/* リスト2:引数の示す変数の中身を2乗する関数 */
void ipower2(int *n)
{
  *n *= *n;
}

/* リスト3:2つの引数の値を入れ替える関数(×) */
iswap1(int a, int b)
{
  int tmp;
  tmp = a;
  a = b;
  b = tmp;
}

/* リスト4:2つの引数の値を入れ替える関数(○) */
iswap2(int *a, int *b)
{
  int tmp;
  tmp = *a;
  *a = *b;
  *b = tmp;
}

int main(void)
{
  int num, ans;
  int x, y;

/* ipower1とipower2の動作を試す */
  num = 10;
  ans = ipower1(num);
  printf("Test of ipower1 : ans = %d\n", ans);
  /* ans = 100 */

  ipower2(&ans);
  printf("Test of ipower2 : ans = %d\n", ans);
  /* ans = 10000 */

/* iswap1とiswap2の動作を試す */
  x = 10;
  y = 100;
  iswap1(x, y);
  printf("Test of iswap1 : x = %d, y = %d\n", x, y);

  x = 10;
  y = 100;
  iswap2(&x, &y);
  printf("Test of iswap2 : x = %d, y = %d\n", x, y);

  return (0);
}

あとがき

hiropの『ちょっと気になる専門用語』~《渡す/積む》

引数を棚に「積む」

関数に引数を設定することを、一般に(引数を)「渡す」と表現します。関数は、「引数を渡して、それを呼び出す」ということになります。呼び出された関数の側では、呼び出し元から渡された引数を「受け取る」と表現します。

また、あまり一般的な表現ではありませんが、引数を渡すことを「積む」、積まれた引数を関数の側で「取り出す」と表現することもあります。これは、関数に対する引数の受け渡しのイメージを「棚に荷物を積み、それを取り出す」という動作になぞらえた表現です。

スタックによる受け渡し

なぜ「棚」なのでしょう? これは、スタック(stack)というコンピュータに備わっている一時的なデータの保存場所のイメージなのです。アセンブリ言語では、push命令でスタックにデータを預け、それをpop命令で取り出します。そしてCでは、関数への引数の受け渡しにスタックを使い、コンパイルされた機械語レベルのプログラムでは、
呼び出し元がスタックに引数をpush
関数を呼び出す
関数の側でスタックから引数をpop
……といった手順で引数を受け渡しします。

この動作を、小さな棚に下から順に荷物を積んでいく――というイメージで捉えた訳です。

CとPascalは順序が逆

stackは「積み重ねた干し草/煙突」といった意味で、棚と言うより縦長のチューブのようなイメージです。複数のデータをpushすると、先にpushしたデータが底に置かれ、その上から新しいデータが貯められていきます。従って、データを取り出す際には最後にpushしたデータから順にpopしていくことになります。いわゆる「先入れ/後出し」方式です。

ちなみに、Cでは引数を後ろからスタックにpushし、関数の側では先頭のデータ(第1引数)から順にpopしていきます。そのため、printf関数のように引数の数が不定の関数を作れるのです(第1引数を調べれば、%の数によって続く引数の数が分かります)。

一方、同じ手続き型言語のPascalでは、引数を先頭から順にスタックにpushし、関数の側では最後の引数から逆順にpopしていきます。