C言語のきほん|引数ポインタの返却と関数チェーン

受け取ったポインタを上手に返して、関数どうしをなめらかにつなげよう

C言語では、関数が受け取ったポインタを加工したあと、そのまま返す書き方があります。
この方法はとても実用的で、標準ライブラリの関数でもよく使われています。たとえば、ある領域に値を書き込んだあと、その領域を指す同じポインタを返してくれれば、その戻り値を次の関数にそのまま渡せます。こうしたつなげ方は、関数チェーンと呼ばれます。

この書き方のよいところは、コードがすっきりして流れを追いやすくなることです。
処理の対象がずっと同じ領域であることが見えやすく、1つの関数で加工し、その結果を続けて別の関数で加工する、という形が自然に書けます。

ただし、便利だからこそ気をつけたい点もあります。
返しているのは新しく作った領域ではなく、呼び出し元から渡された領域へのポインタです。そのため、その領域が本当に有効なのか、NULLではないか、変更してよい領域なのか、といったことをきちんと意識する必要があります。

ここでは、引数で渡されたポインタを返すとはどういうことか、そのメリットは何か、どんな注意が必要かを順番に整理していきます。
さらに、文字列処理を例にして、関数チェーンの考え方や実践的な使い方もやさしく確認していきましょう。

引数で渡されたポインタを返すとはどういうことか

引数で渡されたポインタを返す、というのは、関数が受け取ったアドレスをそのまま戻り値として返すことです。

たとえば、次のような流れです。

  • 呼び出し元が配列や文字列を用意する
  • その先頭アドレスを関数に渡す
  • 関数はその中身を変更する
  • 最後に同じポインタを返す

つまり、関数の中で新しいメモリを作るのではなく、もともと渡された領域をそのまま使って処理する、という考え方です。

これを表で整理すると、次のようになります。

項目内容
受け取るもの呼び出し元が用意した領域へのポインタ
関数の役割その領域の中身を加工する
返すもの受け取ったのと同じポインタ
利点次の関数へそのまま渡しやすい

この方式では、関数が新しく領域を確保するわけではないので、free の責任を増やさずに済むことが多いです。
その代わり、もとの領域の有効期間は呼び出し元に依存します。

シンプルな例で流れを見てみよう

ここでは、配列に 5, 10, 15, 20, 25 を順に入れて、その配列の先頭ポインタを返す関数を作ります。

ファイル名:14_5_1.c

#include <stdio.h>

#define DATA_SIZE 5

int *fill_values(int *arr, int n);

int main(void)
{
    int numbers[DATA_SIZE];
    int *result = fill_values(numbers, DATA_SIZE);

    printf("作成した値: ");
    for (int i = 0; i < DATA_SIZE; i++) {
        printf("%d ", result[i]);
    }
    printf("\n");

    return 0;
}

int *fill_values(int *arr, int n)
{
    if (arr != NULL) {
        for (int i = 0; i < n; i++) {
            arr[i] = (i + 1) * 5;   /* 5, 10, 15... を代入する */
        }
    }

    return arr;   /* 受け取ったポインタをそのまま返す */
}

実行結果例

作成した値: 5 10 15 20 25

このプログラムでは、main 関数で用意した配列 numbers を fill_values 関数に渡しています。
関数の中では、その配列に値を書き込み、最後に同じ配列の先頭アドレスを返しています。

ここで大事なのは、返しているのが新しい配列ではなく、main 関数が最初から持っていた配列へのポインタだということです。

なぜこの方法が使いやすいのか

この方法のよさは、同じデータに対して処理を続けていきやすいところにあります。

たとえば、ある関数で初期化して、次の関数で変換して、さらに別の関数で表示用に整える、といった流れを考えてみましょう。
毎回別の変数に受け取ってもよいのですが、同じポインタを返してくれるなら、そのまま次につなげやすくなります。

そのため、コード全体が次のような考え方で書けるようになります。

書き方イメージ
1段ずつ受け取る途中結果を変数に入れながら進む
つなげて書く戻り値をそのまま次の関数へ渡す

この「つなげて書く」形が、関数チェーンです。

関数チェーンとは何か

関数チェーンとは、ある関数の戻り値をそのまま次の関数の引数に渡していく書き方です。

たとえば、次のような形です。

func3(func2(func1(data)));

この書き方では、まず func1 が実行され、その戻り値が func2 に渡され、さらにその戻り値が func3 に渡されます。

引数で受け取ったポインタをそのまま返す関数は、この書き方ととても相性がよいです。
なぜなら、加工対象の領域が変わらないまま、処理だけを順番に重ねていけるからです。

関数チェーンのイメージを図で理解する

文章だけだと少し見えにくいので、図にすると理解しやすくなります。

この図では、最初に用意した data という配列が、func1、func2、func3 という関数に順番に渡されている様子を表しています。
ポイントは、毎回別の領域を作っているのではなく、同じポインタを返しながら、同じ領域に対して処理を重ねているところです。

この見方ができると、関数チェーンは「データの受け渡しが複雑な書き方」ではなく、「同じ対象に対する処理の連続」だと理解しやすくなります。

文字列処理では特に便利

引数ポインタを返す関数は、文字列処理で特に便利です。
文字列は char 型の配列として扱えるので、配列の先頭アドレスを受け取り、その内容を変更して同じポインタを返す、という形がとても自然だからです。

たとえば、次のような処理は相性がよいです。

  • 大文字を小文字に変換する
  • 空白を別の文字に置き換える
  • 文字列を逆順にする
  • 特定の文字をまとめて加工する

文字列を加工したあと、そのまま同じ文字列を返せば、別の関数につなげて使うことができます。

実践的な文字列の例

文字列中の小文字アルファベットを大文字に変換し、その文字列を返す関数です。

ファイル名:14_5_2.c

#include <stdio.h>
#include <ctype.h>

char *to_upper_text(char *str);

int main(void)
{
    char message[] = "Good Morning!";

    printf("変換前: %s\n", message);
    printf("変換後: %s\n", to_upper_text(message));

    return 0;
}

char *to_upper_text(char *str)
{
    if (str != NULL) {
        for (int i = 0; str[i] != '\0'; i++) {
            str[i] = toupper((unsigned char)str[i]);   /* 小文字を大文字に変換する */
        }
    }

    return str;   /* 同じ文字列へのポインタを返す */
}

実行結果例

変換前: Good Morning!
変換後: GOOD MORNING!

この例でも、関数は新しい文字列を作っていません。
main 関数にある message を直接書き換え、その先頭アドレスをそのまま返しています。

関数チェーンの例

では、関数チェーンの形がわかるように、2つの文字列処理関数をつなげる例を見てみましょう。

ここでは、文字列を大文字にしたあと、末尾に感嘆符を追加するようなイメージで考えたいところですが、固定配列の扱いがやや複雑になるので、ここでは「空白をアンダースコアに置き換える関数」とつなげる例にします。

ファイル名:14_5_3.c

#include <stdio.h>
#include <ctype.h>

char *to_upper_text(char *str);
char *replace_space(char *str);

int main(void)
{
    char title[] = "nice day";

    printf("変換前: %s\n", title);
    printf("変換後: %s\n", replace_space(to_upper_text(title)));

    return 0;
}

char *to_upper_text(char *str)
{
    if (str != NULL) {
        for (int i = 0; str[i] != '\0'; i++) {
            str[i] = toupper((unsigned char)str[i]);
        }
    }

    return str;
}

char *replace_space(char *str)
{
    if (str != NULL) {
        for (int i = 0; str[i] != '\0'; i++) {
            if (str[i] == ' ') {
                str[i] = '_';
            }
        }
    }

    return str;
}

実行結果例

変換前: nice day
変換後: NICE_DAY

このコードでは、まず to_upper_text(title) が実行されて title が大文字になります。
その戻り値、つまり同じ title の先頭ポインタが、そのまま replace_space に渡されます。
そして今度は空白がアンダースコアに変換されます。

こうして、同じ文字列を順番に加工していく流れが自然に書けます。

この書き方の長所

引数ポインタを返す方法には、いくつかの使いやすい点があります。

長所説明
コードが簡潔になる戻り値をそのまま次へ渡せる
同じ対象を加工していることがわかりやすい処理対象が一貫する
新しい領域を作らなくてよい不要なメモリ確保を避けやすい
標準ライブラリの考え方に近い実践的な書き方に慣れやすい

特に、文字列や配列に対して「加工して返す」というスタイルでは、かなり見通しのよいコードになります。

注意しなければいけない点

便利な方法ですが、いくつか大切な注意点があります。

NULLチェックが必要

ポインタ引数を受け取る関数では、まず NULL でないかを確認することが大切です。
NULL のまま中身にアクセスすると、異常終了の原因になります。

そのため、次のような確認は基本になります。

if (str != NULL) {
    /* 安全に処理する */
}

これは配列でも文字列でも同じです。

渡された領域が変更可能である必要がある

文字列処理では特に重要ですが、関数の中で中身を書き換えるなら、その領域は変更可能でなければなりません。

たとえば、char 型の配列なら書き換えられますが、文字列リテラルを直接渡して書き換えようとするのは危険です。
そのため、加工対象は変更可能な配列として用意する、という意識が大切です。

関数の中では寿命を完全には把握できない

引数で受け取ったポインタは、どこで作られた領域なのかが関数の中からは見えにくいことがあります。
それがローカル配列なのか、動的メモリなのか、グローバル変数なのかは、呼び出し側しだいです。

つまり、関数内では「この領域はいつまで有効か」を完全には判断できません。
だからこそ、呼び出し側が有効な領域を渡すこと、そして関数側が危険な使い方をしないことが大切になります。

返ってきたポインタが新しい領域だと勘違いしない

この形式の関数は、あくまで同じポインタを返しているだけです。
新しく複製した文字列や新しい配列を返しているわけではありません。

そのため、戻り値を見て「別のデータができた」と思い込むと混乱しやすいです。
実際には、もとの領域そのものが書き換わっています。

図で見る「同じポインタを返す」という感覚

この図では、main 関数が持っている message という文字列配列が、そのまま関数に渡され、加工されたあと、同じアドレスとして返される様子を表しています。

ここで特に見てほしいのは、「新しい文字列が別に作られているわけではない」という点です。
関数は同じ領域の中身を書き換えて、その領域を指すポインタを返しています。
この理解があると、関数チェーンの動きもとてもわかりやすくなります。

実践問題

次の仕様に従って関数を作成し、main 関数から呼び出して結果を確認してください。

関数宣言

char *replace_dot_with_dash(char *str);

機能
変更可能な文字列を引数に取り、文字列中の . をすべて - に置き換える。

返却値
str の値を返す。

実行結果例

変換前: file.name.txt
変換後: file-name-txt

解答例

ファイル名:14_5_4.c

#include <stdio.h>

char *replace_dot_with_dash(char *str);

int main(void)
{
    char text[] = "file.name.txt";

    printf("変換前: %s\n", text);
    printf("変換後: %s\n", replace_dot_with_dash(text));

    return 0;
}

char *replace_dot_with_dash(char *str)
{
    if (str != NULL) {
        for (int i = 0; str[i] != '\0'; i++) {
            if (str[i] == '.') {
                str[i] = '-';   /* ピリオドをハイフンに置き換える */
            }
        }
    }

    return str;   /* 同じ文字列へのポインタを返す */
}

解説

この問題では、文字列の中を先頭から順番に調べ、. を見つけたら - に置き換えています。
文字列の終わりはヌル文字で判断しているので、配列の長さを別に持たなくても処理できます。

また、この関数は最後に str を返しているので、呼び出し側では戻り値をそのまま printf に渡すこともできます。
この形は、引数ポインタを返す関数らしい書き方になっています。

実践問題

複数の文字列処理関数を組み合わせて使う練習になる内容です。
次の仕様に従って関数を作成し、main 関数から呼び出して結果を確認してください。

関数宣言

int is_same_text_ignore_case(const char *str1, const char *str2);

機能
str1 と str2 が指す文字列を、大文字と小文字を区別せずに比較する。
同じ内容なら 1、異なるなら 0 を返す。

補足
・比較の前に、2つの文字列をそれぞれ動的メモリへコピーする
・コピーした文字列を小文字に変換してから比較する
・文字列のコピーには strcpy、比較には strcmp を使ってよい
・小文字変換には、自分で作成した関数を利用してよい

実行結果例

"Apple" と "apple" は同じです。
"Hello" と "World" は同じではありません。

解答例

ファイル名:14_5_5.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

char *to_lower_text(char *str);
int is_same_text_ignore_case(const char *str1, const char *str2);

int main(void)
{
    if (is_same_text_ignore_case("Apple", "apple")) {
        printf("\"Apple\" と \"apple\" は同じです。\n");
    } else {
        printf("\"Apple\" と \"apple\" は同じではありません。\n");
    }

    if (is_same_text_ignore_case("Hello", "World")) {
        printf("\"Hello\" と \"World\" は同じです。\n");
    } else {
        printf("\"Hello\" と \"World\" は同じではありません。\n");
    }

    return 0;
}

char *to_lower_text(char *str)
{
    if (str != NULL) {
        for (int i = 0; str[i] != '\0'; i++) {
            str[i] = tolower((unsigned char)str[i]);   /* 英大文字を小文字に変換する */
        }
    }

    return str;
}

int is_same_text_ignore_case(const char *str1, const char *str2)
{
    char *copy1;
    char *copy2;
    int result;

    copy1 = malloc(strlen(str1) + 1);
    copy2 = malloc(strlen(str2) + 1);

    if (copy1 == NULL || copy2 == NULL) {
        free(copy1);
        free(copy2);
        return 0;
    }

    strcpy(copy1, str1);
    strcpy(copy2, str2);

    result = strcmp(to_lower_text(copy1), to_lower_text(copy2)) == 0;

    free(copy1);
    free(copy2);

    return result;
}

解説

この問題では、もとの文字列が const char * で渡されているため、直接書き換えないようにしています。
そこで、まず malloc で動的メモリを確保し、文字列をコピーしてから小文字変換を行っています。

そのあと、to_lower_text(copy1) と to_lower_text(copy2) のように、引数ポインタを返す関数をそのまま strcmp の引数へ渡しています。
これが、関数チェーンの考え方を活かした書き方です。

このように、引数ポインタを返す関数は、単体でも便利ですが、複数の処理をつなげるとさらに使いやすさが見えてきます。

学習のときに意識したいポイント

このテーマでは、次の点を意識すると理解しやすくなります。

注目する点見るべき内容
どの領域を処理しているか呼び出し元が用意した配列や文字列
何を返しているか同じ領域へのポインタ
何が便利か関数チェーンしやすいこと
何に注意するかNULLチェックと有効な領域かどうか

特に大切なのは、「戻り値は新しいデータではなく、同じデータへのポインタである」という見方です。
この感覚がつかめると、標準ライブラリの関数の戻り値もずっと理解しやすくなります。

最後に押さえておきたいこと

引数で渡されたポインタを返す関数は、C言語らしい実用的な書き方の1つです。
同じ領域をそのまま加工し、同じポインタを返すことで、コードを簡潔にしながら処理をなめらかにつなげることができます。

特に文字列処理では、この考え方がとてもよく使われます。
ただし、NULLチェックをすること、変更可能な領域を渡すこと、戻り値が新しい領域ではないことをきちんと理解しておくことが大切です。

関数チェーンは最初は少しだけ慣れが必要ですが、「同じデータに処理を重ねているだけ」と考えると、ぐっとわかりやすくなります。
この視点を持ちながら読むと、標準ライブラリの関数設計にも自然に目が向くようになります。