ポインタ③(引数や戻り値への利用) | Programming Place Plus C言語編 第33章

トップページC言語編

このページの概要

以下は目次です。


関数に大きなデータを渡す

ここからしばらく、ポインタを引数や戻り値に使うことについて説明していきます。まずは、大きなデータの受け渡しを目的とする利用について見ていきましょう。

関数に引数を渡すことや、戻り値を返すことも、結局は代入操作と同様で、コピーをしています。コピーする量が大きければ、それだけ処理に時間が掛かりますから、大きなデータのコピーには注意が必要です。

C言語において、大きなデータといえば、配列や構造体が思い浮かびます。しかしこれまでに何度も書いているように、配列はそのまま関数に受け渡せません。この話題はあとであらためて触れます

一方、構造体変数は代入操作が行えます(第26章)。そのため、構造体変数は、引数や戻り値に使えます。

struct Student_tag s;
func(s);

構造体の場合、これが可能ではありますが、前述したように、データが大きいと処理時間を浪費します。そこで、構造体を関数に渡す場合は、ポインタとして渡すという方法が使えます。

struct Student_tag s;
func(&s);

実際のプログラムで確認してみましょう。

#include <stdio.h>

#define STUDENT_NAME_LEN 32         // 生徒の名前データの最大長

// 生徒のデータ
typedef struct {
    char  name[STUDENT_NAME_LEN];   // 名前
    int   grade;                    // 学年
    int   class;                    // 所属クラス
    int   score;                    // 得点
} Student;


void print_student_data(const Student* student);

int main(void)
{
    const Student student = {"Saitou Takashi", 2, 3, 80};

    print_student_data(&student);
}

/*
    生徒のデータを出力する。
    引数:
        student: 出力するデータを集めた構造体変数。
*/
void print_student_data(const Student* student)
{
    printf("name: %s\n", student->name);
    printf("grade: %d\n", student->grade);
    printf("class: %d\n", student->class);
    printf("score: %d\n", student->score);
}

実行結果:

name: Saitou Takashi
grade: 2
class: 3
score: 80

構造体のポインタ型を渡せるようにするため、print_student_data関数の仮引数は「Student*」という型名になっています。typedef を使わないのなら struct Student_tag* です。また、関数内でポインタが指し示している先にある値を書き換えないので、const を付けてあります。const を付けられる場面では意識的に付けていきましょう。

ポインタを経由して構造体のメンバを参照する場合は、student->name のように -> という演算子を使用できます。これはアロー演算子 (arrow operator) と呼ばれます。この辺りの話題は、第37章であらためて取り上げるので、今のところは、これで参照できるということだけ覚えておいてください。

なお、構造体を戻り値で返したいときにポインタを使うと、正しく動作しない恐れがあります。これは構造体が悪いのではなく、ポインタの使用上の注意点です。この話題はあとで触れます

関数に配列を渡す

配列は関数に渡せないという話ですが、まずは試してみましょう。

#include <stdio.h>

#define ARRAY_SIZE 5

void print_array(int array[ARRAY_SIZE]);

int main(void)
{
    int array[ARRAY_SIZE] = {0, 1, 2, 3, 4};

    print_array(array);
}

/*
    配列の各要素を出力する。
*/
void print_array(int array[ARRAY_SIZE])
{
    for (int i = 0; i < ARRAY_SIZE; ++i) {
        printf("%d ", array[i]);
    }
    printf("\n");
}

実行結果:

0 1 2 3 4

このとおり、実は動きます。しかし、意図している動作は「配列を渡す」ことですが、実際には「ポインタに変換されて渡されています」

ここで、少し改造して確かめておきましょう。もし、配列をそのまま渡せているのだとすれば、print_array関数の中で、array の要素を書き換えても、それはコピーですから、呼び出し側の配列には影響を与えないはずです。

#include <stdio.h>

#define ARRAY_SIZE 5

void print_array(int array[ARRAY_SIZE]);

int main(void)
{
    int array[ARRAY_SIZE] = {0, 1, 2, 3, 4};

    print_array(array);
    printf("%d\n", array[3]);  // 配列のコピーを渡したなら 3、メモリアドレスを渡したなら 999 になるはず
}

/*
    配列の各要素を出力する。
*/
void print_array(int array[ARRAY_SIZE])
{
    for (int i = 0; i < ARRAY_SIZE; ++i) {
        printf("%d ", array[i]);
    }
    printf("\n");

    array[3] = 999;
}

実行結果:

0 1 2 3 4
999

実行結果にあるように、print_array関数の中で代入した 999 が、呼び出し元の方の array に影響を与えていることが分かります。これは、配列のコピーを渡しているのではなく、配列を指すポインタを渡しているからです。

以下のようにすれば、もっと直接的に確かめることもできるでしょう。

#include <stdio.h>

#define ARRAY_SIZE 5

void print_array(int array[ARRAY_SIZE]);

int main(void)
{
    int array[ARRAY_SIZE] = {0, 1, 2, 3, 4};

    print_array(array);
    printf("%p\n", array);
}

/*
    配列の各要素を出力する。
*/
void print_array(int array[ARRAY_SIZE])
{
    for (int i = 0; i < ARRAY_SIZE; ++i) {
        printf("%d ", array[i]);
    }
    printf("\n");

    printf("%p\n", array);
}

実行結果:

0 1 2 3 4
002EFE3C
002EFE3C

print_array で出力した array のメモリアドレスと、呼び出し元の array のメモリアドレスが一致していることが分かります。このように、関数へは、配列のコピーではなくて、配列を指すポインタが渡されています

結局、以下の2つの関数宣言は同じ意味です。

void func(int array[]);
void func(int* array);

[] の中に要素数を書いても、省略しても構いません。いずれにしても、配列としては見てくれないので無意味です。

これもまた、配列とポインタが混同される原因の1つだと思われます。まるで、int[] と int* が置き換え可能なもののように見えますが、両者が同じ意味になるのは、関数の仮引数のところだけです

なお、構造体は引数に使えるので、メンバに配列が含まれている構造体だったらどうなるのか、という疑問もあります。この場合は、渡しているものはあくまで構造体という塊なので、問題なく構造体全体のコピーが渡されます。

struct MyStruct {
    int array[1000];
};

struct MyStruct s;
func(s);

仮引数の配列に対する修飾子

関数の仮引数を、配列の形で記述する場合に、いくつかの修飾子を付加できます。ただし、この機能は、Visual Studio は対応していません

この機能は C99規格で追加されたものですが、あまり知られている機能ではないと思います。また、C++ にも導入されていません。

たとえば、次のような形式で記述します。

void func(int array[修飾子 10]);

「修飾子」の部分に使用できるのは、const、restrict(第57章)、volatile、static の4つのいずれか、あるいはこれらの組み合わせです。static 以外の3つは、以下のように変形したときと同じ意味になります。

void func(int* 修飾子 array);

そのため、const の場合なら、関数定義の中で、仮引数 array を書き換えることが禁じられます(第32章)。

#include <stddef.h>

void func(int array[const 10]);

int main(void)
{
    int a[10];

    func(a);
}

void func(int array[const 10])
{
    array = NULL;  // コンパイルエラー
}

restrict と volatile の場合も、ポインタ変数の宣言時にこれらの修飾子を使ったときと同じです。

static の場合は、実引数に指定された配列が、仮引数で指定した数値以上の要素数を持っていなければならないことを意味します。clang 5.0.0 では、以下のプログラムで警告を出力します。

#include <stddef.h>

void func(int array[static 10]);

int main(void)
{
    int a1[10];
    int a2[5];

    func(a1);    // OK
    func(a2);    // 不適切。実引数側の要素数の方が少ない
    func(NULL);  // 不適切。配列でない
}

void func(int array[static 10])
{
}


関数に値を書き換えてもらう

関数にポインタを渡すことによって実現できることの1つに、関数にポインタが指し示す先にある値を書き換えてもらうというものがあります。ここでは、2つの変数の値交換する関数を作ってみましょう。第28章の練習問題では、これをマクロで定義しましたが、今回は関数です。

#include <stdio.h>

void swap(int* a, int* b);

int main(void)
{
    int num1 = 10;
    int num2 = 20;

    swap(&num1, &num2);
    printf("%d %d\n", num1, num2);
}

/*
    2つの int型変数の値を交換する。
*/
void swap(int* a, int* b)
{
    int work = *a;
    *a = *b;
    *b = work;
}

実行結果:

20 10

swap関数の仮引数は、2つの int型のポインタです。実引数に、int型の変数を指すポインタを2つ渡せば、それぞれが指し示す先にある変数の値が交換されます。

swap関数は、関数の呼び出し元にある変数の値を、関数の中で書き換えている訳です。このような使い方は有用ですが、ヌルポインタが渡されてきた場合の対応を考えないといけません。先ほどのサンプルプログラムでは何も考慮されていないため、*a のように間接参照したときに未定義の動作が起こり得ます。きちんと対処するなら、たとえば assertマクロを使って停止させると良いでしょう。

void swap(int* a, int* b)
{
    assert(a != NULL && b != NULL);

    int work = *a;
    *a = *b;
    *b = work;
}

ところで、swap関数のように、引数で渡されてきたポインタを経由して、値を書き換える場合には、const int* a」のように、const を付加しないようにしなければなりません。const を付けるべき場面で、付けてはならない場面の感覚が分かるでしょうか?

void swap(const int* a, const int* b)
{
    assert(a != NULL && b != NULL);

    int work = *a;
    *a = *b;    // コンパイルエラー
    *b = work;  // コンパイルエラー
}

戻り値が複数欲しいとき

関数にポインタを渡すことによって実現できることがもう1つあります。それは、複数の戻り値を返す関数を、引数を使って実現することです。

知ってのとおり、C言語の関数では、戻り値は 0個(void)か、1個のいずれかです。しかし、どうしても2個以上の情報を返したい場面はよくあります。

たとえば、次のような表があるとします。

1 2 3 4 5
6 7 8 9 10
11 12 13 14 15
16 17 18 19 20
21 22 23 24 25

このとき、1~25 の値を指定し、その数値がある行と列の番号を返す関数を作るとしましょう。

#include <stdio.h>

void get_col_and_row(int num, int* col, int* row);

int main(void)
{
    int col, row;

    get_col_and_row(17, &col, &row);
    printf("%d %d\n", col, row);

    get_col_and_row(21, &col, &row);
    printf("%d %d\n", col, row);
}

/*
    表の中の num のある位置の行番号と列番号を返す。
    引数:
        num:    表の中にある番号。1~25
        col:    行番号を受け取るメモリアドレス。0基準
        row:    列番号を受け取るメモリアドレス。0基準
*/
void get_col_and_row(int num, int* col, int* row)
{
    assert(1 <= num && num <= 25);
    assert(col != NULL);
    assert(row != NULL);

    *col = (num - 1) % 5;
    *row = (num - 1) / 5;
}

実行結果:

1 3
0 4

行番号と列番号を 1つの関数から返したくても、戻り値では2つの情報を同時に返せません。そこで、ポインタを利用して、引数を経由して結果を返してもらっています。この手法はいうなれば、関数に、結果を入れる入れ物だけを渡して、「結果はここに入れておいてくれ」という感覚です。

ちなみに、関数から 2つ以上の情報を返す方法はいくつかあります。

  1. ポインタを渡して、結果を格納してもらう(今回取り上げた方法)
  2. 構造体を定義して、構造体変数に結果を入れて戻り値で返す
  3. グローバル変数を用意しておき、そこに結果を格納する

構造体を作る方法は、実際にそうすることもあります。たとえば、ある点の X座標と Y座標をまとめて返すといったとき、Point構造体のようなもので返すのが簡単です。この方法は、ポインタが登場しないため、ヌルポインタのことを気にせずに済むという利点がありますが、構造体が大きい場合は、コピーのコストを考えなければなりません。

グローバル変数を経由させる方法は、基本的に避けましょう。何度か書いているとおり、スコープは極力狭くすべきであって(第22章)、グローバル変数を検討するのは最後の手段です。他の手法を優先しましょう。

ローカル変数を返すとき

戻り値としてポインタを使う場合には、注意を要します。次のプログラムは正しく動作するでしょうか?

#include <stdio.h>
#include <assert.h>

char* get_string(int max);

int main(void)
{
    char* str = get_string(5);
    puts(str);  // 受け取った文字列を出力
}

/*
    標準入力から max以下の文字数の文字列を受け取る。
    引数:
        max:    最大文字数。1~255
    戻り値:
        受け取った文字列。
*/
char* get_string(int max)
{
    assert(1 <= max && max <= 255);

    printf("%d文字以内の文字列を入力してください。\n", max);

    char str[256];
    fgets(str, max+1, stdin);     // 改行文字のことを考えて +1

    return str;
}

このプログラムは、非常によくありがちなミスが混ざっており、正しく動作する保証がありません。

問題なのは、get_string関数の戻り値です。return文が返している str は、自動記憶域期間を持つローカル変数ですから、関数から抜け出してしまった後は、もうメモリ上に残っている保証がないのです。そのため、呼び出し側が受け取ったポインタが指し示す先には、もう str はないと考えるべきなのです。

コンパイル時に警告ぐらいは報告してくれるかもしれませんが、それもコンパイラ次第ですし、警告であれば実行できてしまいます。こういうエラーレベルの警告もあるので、警告は無視してはいけません。

今回のような、ローカル変数を指すポインタを返してしまう間違いへの対応策としては、以下のものがあります。

  1. 呼び出し元の方で変数を用意し、ポインタを渡し、そこに結果を格納させる
  2. 静的ローカル変数(第22章)に変更する
  3. グローバル変数に結果を格納するように変更する
  4. グローバル変数に結果を格納して、そのメモリアドレスを返すように変更する
  5. 動的メモリ割り当てを行う(第35章で説明)

例によって、グローバル変数を使う方法は、最後の手段にすべきです。

静的ローカル変数を使う場合は、同じ関数を2回以上呼び出したとき、前回の値が残っていることに注意を払わないといけません。これは恐らく、グローバル変数を使った場合も同じです。

動的メモリ割り当てに関しては、第35章で説明します。ただ、この方法も(この場面では)あまり有用ではありません。

1番目の方法が有力です。これは結局、swap関数の例と同じことです。

#include <stdio.h>
#include <assert.h>

char* get_string(char* str, int max);

int main(void)
{
    char str[256];  // 呼び出し元で変数を準備

    get_string(str, 5);  // str に結果を入れてもらう
    puts(str);          // 受け取った文字列を出力
}

/*
    標準入力から max以下の文字数の文字列を受け取る。
    引数:
        str:    文字列を受け取る配列。
        max:    最大文字数。1~255
    戻り値:
        受け取った文字列。
*/
char* get_string(char* str, int max)
{
    assert(str != NULL);
    assert(1 <= max && max <= 255);

    printf("%d文字以内の文字列を入力してください。\n", max);
    fgets(str, max+1, stdin);     // 改行文字のことを考えて +1

    return str;
}

相変わらずポインタを返しているようですが、今度はそもそも呼び出し元にある変数を指しているポインタを返しているだけなので、この場合は問題ありません。このように、実引数で渡したポインタをそのまま返すような関数の設計は、関数を使う側の利便性を高める効果があります。

たとえば、次のように呼び出せます。

puts(get_string(str, 5));

このように、関数の戻り値を、他の関数の実引数に使うという連鎖的な呼び出しが可能になります。もちろん、かえって分かりづらくなるのならやめた方が良いですが、こういう考え方もあります。


練習問題

問題① 関数の仮引数において、const型修飾子を以下のように使うことはよくあります。

void func(const MyStruct* s);

一方で、次のように使うことはあまりありません。

void func(MyStruct* const s);

このような、* の後ろ側に書くタイプの const型修飾子が、関数の仮引数に使われることが少ない理由はなぜでしょう。

問題② strcpy関数を自作してください。

問題③ strcmp関数を自作してください。

問題④ 文字列の中から特定の文字を探し出し、そのメモリアドレスを返す strchr関数という標準ライブラリ関数があります。この関数は、次のように宣言されています。

char* strchr(const char* s, int c);

文字列s の先頭から順番に文字を調べ、c と一致するものが登場したら、そのメモリアドレスを返します。もし、文字列s の末尾までの間に一致するものが登場しなかったら、ヌルポインタを返します。c は int型ですが、関数内部では char型として比較されます。また、文字列s の末尾にある ‘\0’ を探し出すことも可能です。
strchr関数と同じことをする関数を自作してください。

問題⑤ 2つの文字列を連結する strcat関数という標準ライブラリ関数があります。この関数は、次のように宣言されています。

char* strcat(char* restrict s1, const char* restrict s2);

restrict については、第57章で取り上げます。動作に影響はないので、今は無視して問題ありません。

s1 の末尾に s2 を連結させます。戻り値は s1 と同じ値をそのまま返します。この標準ライブラリ関数と同じことをする関数を自作してください。


解答ページはこちら

参考リンク


更新履歴

≪さらに古い更新履歴を展開する≫



前の章へ (第32章 ポインタ②(配列や文字列との関係性))

次の章へ (第34章 ポインタ④(バイト単位の処理))

C言語編のトップページへ

Programming Place Plus のトップページへ



はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る