パソコン活用研究シリコンバレー(C、C++、の活用研究)

          関数の引数と返値

(準備中)

C言語の関数のデータの受け渡しについては、変数の有効範囲と関数のデータの受け渡しにも
書きましたが、ここでもう少し突っ込んだ話を加えて整理しておきます。

説明用に使うのは以下のコードです。
以下のコードには、4つの関数 int* invalidplus(int), int* plus(int), int plus1(int), void vplus(int*) が
ありますが、いずれも受け取った値に1を加えた値を返す関数です。
invalidplus()は未定義動作を含む関数ですが、比較のためにあえて入れました。

これらの関数の動作を比較することにより、変数が確保する領域、そのスコープの違いをテストし、
関数の引数の安全な渡し方と返り値の安全な戻し方を検証してみます。

<pfunc.c>

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

int* invalidplus();
int* plus();
int plus1();
void vplus();


int* invalidplus(int x) {
        int *y, temp;
        y=&temp;
        *y=x+1;
        printf("y:%d, %d\n",y,*y);
        return y;
}

int* plus(int x) {
        int *y;
        y=(int*)malloc(sizeof(int));
        *y=x+1;
        printf("y:%d, %d\n",y,*y);
        return y;
}

int plus1(int x){
        int y;
        y=x+1;
        printf("y:%d, %d\n",&y,y);
        return y;
}


void vplus(int *y) {
        *y=*y+1;
        printf("y:%d, %d\n",y,*y);
}


int main(void) 
{
int i,*ret,*invalidret;
        
        invalidret=invalidplus(5);
        printf("invalidret:%d, %d\n",invalidret,*invalidret);
        
        ret=plus(5);
        printf("ret:%d, %d\n",ret,*ret);
        free(ret);
        
        i=plus1(6);
        printf("i:%d, %d\n",&i,i);
        
        printf("invalidret:%d, %d\n",invalidret,*invalidret);
        
        i=7;
        vplus(&i);
        printf("i:%d, %d\n",&i,i);
        
        
return 0;
}

 



1.関数の宣言

関数の宣言は、
データ型 関数名( );
で宣言し、関数の本体はその後に実装します。

記憶クラスを先頭につけることもあります。

説明用のpfunc.cでは
int* invalidplus();
int* plus();
int plus1();
void vplus();
の4行が宣言の部分です。

voidは返り値を持たない関数のデータ型です。

2.関数の本体

関数の宣言の後に、4つの関数の本体を実装しています。

int* invalidplus(int x) {
int *y, temp;
y=&temp;
*y=x+1;
printf("y:%d, %d\n",y,*y);
return y;
}

int* plus(int x) {
int *y;
y=(int*)malloc(sizeof(int));
*y=x+1;
printf("y:%d, %d\n",y,*y);
return y;
}

int plus1(int x){
int y;
y=x+1;
printf("y:%d, %d\n",&y,y);
return y;
}

void vplus(int *y) {
*y=*y+1;
printf("y:%d, %d\n",y,*y);
}


全て引数で受け取った数値に1を加えて返すというシンプルな動作の関数ですが、
ここではいろいろテストするために4つの関数を実装しました。

(1) 関数の説明

@invalidplus()
これはやってはいけない(動作が保証されない)例として作りました。
関数の中でローカルのポインタ変数*yを作り、yのポインタをreturnします。

呼び出し側にはyのポインタ(アドレス)が返り値として渡されますが、
このローカルのポインタ変数*yのポインタ(アドレス)が指すメモリは関数が終了すると破壊されますので、
動作は保証されなくなります。

A plus()
関数の中でローカルのポインタ変数*yを宣言しますが、yのためのメモリはmallocでヒープ領域に確保します。
こうしてヒープ領域に確保したメモリはfree()等で解放しない限り、存続しますので、あとでいつでも参照できます。
関数の中でローカルのポインタ変数を宣言し、その値(ポインタ)を呼び出し元に返すときは、malloc等で領域確保する
ことが必要になります。

(※)関数の中のローカル変数はC言語では通常スタック領域に確保され、関数を抜けるとその領域は解放されるので、
別の変数で上書きされると値は書き換わってしまう。


B plus1()
普通のローカル変数yを使い、yの値をreturnします。オーソドックスな返り値の返し方です。
関数を抜けるとyの値は破壊されますが、呼び出し側の変数 i にはコピーされた値が渡されますので、
問題ありません。


C vplus()
引数にポインタ変数を使い、引数で受け渡しするタイプの関数です。
なのでreturnはありません。
関数の中で宣言されるローカルのポインタ変数yのポインタ(アドレス)は呼び出し元の変数 i の
ポインタ(アドレス)と同一です。呼び出し元の変数 i が有効な限りこのメモリは存続するので
問題ありません。

(2) 実行結果

@invalidplus()
関数の中のローカルのポインタ変数*yのポインタ(アドレス)は170371になっています。
呼び出し側の変数invalidretにもこのアドレスが渡されており、値として6が保持されています。
これは関数が終了した直後に表示させたので、偶々値が破壊されずに保持されていたにすぎません。
その後いろいろ作業してからinvalidretを調べたところ(赤の矢印のココ注目の行)、1703716番地には
別の値4198943が入っていました。


A plus()
mallocで確保したメモリは36515784と先ほどとは全く違う場所になっています。
こちらはヒープ領域にメモリを確保したものと思います。
呼び出し側の変数retのポインタ(アドレス)はそこになっています。

B plus1()
関数の中で使ったローカル変数のアドレスは1703716(最初のinvalidplusでyが使ったのと同じですね)に
なっています。
それに対して、呼び出し側の変数 i のアドレスは1703740と別の領域を使っており。関数の中のローカル変数y
とは別の領域が使われています。
値はコピーして新しい領域に保存されたことがわかります。


C vplus()
関数の中のローカルのポインタ変数yが指しているのは、呼び出し側の変数 i が使っている領域(1703740)ですね。




3.文字列の受け渡し

上記では数値の受け渡しを見てきましたが、ここでは文字列の受け渡しについてテストしてみます。

@ 関数char* s1()
関数の中で配列に文字列を保存し、その配列のアドレスをreturnする。これはCの未定義動作になります。

A 関数char* s2()
関数の中でポインタ変数に文字列を保存し、そのポインタをreturnする。

B 関数char* s3()
関数の中で、mallocで確保した領域に文字列を保存し、その確保した領域のポインタをreturnする。

<pfunc2.c>

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

char* s1();  //@
char* s2();  //A
char* s3();  //B

/* @ */
char* s1(void) {
    char s[4] = "Tom";
    printf("s1 local : %p %s\n", (void*)s, s);
    return s;  // 未定義動作
}

/* A */
char* s2(void) {
    char *s = "Tom";
    printf("s2 static: %p %s\n", (void*)s, s);
    return s;
}

/* B */
char* s3(void) {
    char *s = malloc(4);
    strcpy(s, "Tom");
    printf("s3 heap  : %p %s\n", (void*)s, s);
    return s;
}

int main(void) {
        char *p1,*p2,*p3;
    p1 = s1();
        printf("p1: %p %s\n", (void*)p1, p1); 
    p2 = s2();
        printf("p2: %p %s\n", (void*)p2, p2);
    p3 = s3();
    printf("p3: %p %s\n", (void*)p3, p3);
    printf("p1: %p %s\n", (void*)p1, p1); // UB
    printf("p2: %p %s\n", (void*)p2, p2);
    printf("p3: %p %s\n", (void*)p3, p3);

    free(p3);
}

実行結果は以下の通り。
@の配列で確保される領域は自動領域(一般的にスタック領域) 以下の実行例では0019FF28
main関数のなかで、p1は0019FF28を指していますが、文字列は既に壊れています。
@の関数のなかでchar* s[4]で確保した領域のスコープは関数を抜けると終了し、別のデータで上書きされてしまった
ものと考えられます。
再度p1を表示したときは更に異なるデータに書き換えられています。

Aの関数では、char *s="Tom" と宣言していますが、この場合文字列Tomは静的領域(グローバル領域)に作成されるようです。
従ってmain関数に戻ってp2に代入しても正常にTomが表示されます。

Bはmallocで確保した領域、ヒープ領域にTomが保存されます。ヒープ領域に確保された領域はfreeされない限り
存続するので、main関数に戻ってp3に代入しても正常にTomが表示されます。



TopPage