C
JavaScript
C#

関数パラメータの参照渡しとはどういうことなのか?

More than 1 year has passed since last update.

関数パラメータの参照渡しとはどういうことなのか?

関数呼び出しで、関数(メソッド)のパラメータの渡し方には「値渡し」と「参照渡し」というのがあります。

ただ、Java や JavaScript には言語仕様としての参照渡しはなく、常に値渡しになります。

参照渡しの例ですが、最初に C# を例にして説明します。次のサンプルは2つのパラメータの値を交換するのに参照渡しを使っています。

using System;

public class SwapFunc
{
  static void Main(String[] args)
  {
    int x = 0;
    int y = 1;

    // x と y の値を交換
    Swap(ref x, ref y);

    Console.WriteLine("x = {0:d}, y = {1:d}\n", x, y);
  }

  // x と y の値を交換するメソッド
  static void Swap(ref int x, ref int y)
  {
    var u = x;
    x = y;
    y = u;
  }
}

これの IL (中間コード) は次のようになります。

コメントに動作を書きましたが、正確かどうかは別として、スタックマシン (仮想マシン) を使って、スタックフレーム上のデータをプッシュしたりポップしたりして計算を行っています。

このまま実行すると遅いので、通常、さらにネイティブコードに変換されて実行されます。

参照渡しに関わる部分は Main では、IL_0005 と IL_0006 のあたりです。ここで、スタックフレーム上のデータ x, y のアドレスをスタックに積んでいます。

関数 Swap 側で参照渡しに関わる部分は、ldind 命令を使っているあたりです。この命令は指定したオペランドの内容をアドレスとみなして、そのアドレスの行った先のデータをスタックに積んでいます。

//  Microsoft (R) .NET Framework IL Disassembler.  Version 4.0.30319.18020
//  Copyright (c) Microsoft Corporation. All rights reserved.



// Metadata version: v4.0.30319
.assembly extern mscorlib
{
  .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )                         // .z\V.4..
  .ver 4:0:0:0
}
.assembly Swap
{
  .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilationRelaxationsAttribute::.ctor(int32) = ( 01 00 08 00 00 00 00 00 ) 
  .custom instance void [mscorlib]System.Runtime.CompilerServices.RuntimeCompatibilityAttribute::.ctor() = ( 01 00 01 00 54 02 16 57 72 61 70 4E 6F 6E 45 78   // ....T..WrapNonEx
                                                                                                             63 65 70 74 69 6F 6E 54 68 72 6F 77 73 01 )       // ceptionThrows.
  .hash algorithm 0x00008004
  .ver 0:0:0:0
}
.module Swap.exe
// MVID: {DDFC4E86-9AF5-4AC3-927E-267DAB2AEE1E}
.imagebase 0x00400000
.file alignment 0x00000200
.stackreserve 0x00100000
.subsystem 0x0003       // WINDOWS_CUI
.corflags 0x00000001    //  ILONLY
// Image base: 0x04700000


// =============== CLASS MEMBERS DECLARATION ===================

.class public auto ansi beforefieldinit SwapFunc
       extends [mscorlib]System.Object
{
  .method private hidebysig static void  Main(string[] args) cil managed
  {
    .entrypoint
    // コード サイズ       39 (0x27)
    .maxstack  3
    .locals init (int32 V_0,
             int32 V_1)
    IL_0000:  nop
    IL_0001:  ldc.i4.0  ' 0 をプッシュ。
    IL_0002:  stloc.0  ' 0 をポップしスタックフレーム V_0 に保存
    IL_0003:  ldc.i4.1  ' 1 をプッシュ。
    IL_0004:  stloc.1  ' 1 をポップしてスタックフレーム V_1 に保存
    IL_0005:  ldloca.s   ' V_0  V_0 のアドレスをプッシュ。
    IL_0007:  ldloca.s   ' V_1  V_1 のアドレスをプッシュ。
    IL_0009:  call       void SwapFunc::Swap(int32&,  ' Swap をコール
                                             int32&)
    IL_000e:  nop   何もしない。
    IL_000f:  ldstr      "x = {0:d}, y = {1:d}\n"  ' フォーマットのアドレスをプッシュ
    IL_0014:  ldloc.0  ' V_0 の値をプッシュ。
    IL_0015:  box        [mscorlib]System.Int32 ' スタックのトップをボクシング
    IL_001a:  ldloc.1  ' V_1 の値をプッシュ。
    IL_001b:  box        [mscorlib]System.Int32 ' スタックのトップをボクシング
    IL_0020:  call       void [mscorlib]System.Console::WriteLine(string, ' Console.WriteLine をコール
                                                                  object,
                                                                  object)
    IL_0025:  nop  ' 何もしない。
    IL_0026:  ret  ' もどる。
  } // end of method SwapFunc::Main

  .method private hidebysig static void  Swap(int32& x,
                                              int32& y) cil managed
  {
    // コード サイズ       12 (0xc)
    .maxstack  2
    .locals init (int32 V_0)
    IL_0000:  nop
    IL_0001:  ldarg.0  ' 引数0をプッシュ。
    IL_0002:  ldind.i4  ' スタックトップをアドレスとしてその行った先の内容をプッシュ。
    IL_0003:  stloc.0  ' ローカル変数 0 に格納。
    IL_0004:  ldarg.0  ' 引数0をプッシュ。
    IL_0005:  ldarg.1  ' 引数1をプッシュ。
    IL_0006:  ldind.i4  ' スタックトップをアドレスとして、その行った先の内容をプッシュ。
    IL_0007:  stind.i4  ' スタックトップをアドレスとして、その行った先へストア。
    IL_0008:  ldarg.1 ' 引数1をプッシュ。
    IL_0009:  ldloc.0 ' ローカル変数0をプッシュ。
    IL_000a:  stind.i4 ' その行った先の内容をプッシュ。
    IL_000b:  ret
  } // end of method SwapFunc::Swap

  .method public hidebysig specialname rtspecialname 
          instance void  .ctor() cil managed
  {
    // コード サイズ       7 (0x7)
    .maxstack  8
    IL_0000:  ldarg.0
    IL_0001:  call       instance void [mscorlib]System.Object::.ctor()
    IL_0006:  ret
  } // end of method SwapFunc::.ctor

} // end of class SwapFunc


// =============================================================

// *********** 逆アセンブルが完了しました ***********************
// 警告: Win32 リソース ファイル C:\workspace\dotNET\IL\Swap\Swap.res を作成しました。

次に、C 言語の例を見てみます。

C 言語の場合、VM (仮想マシン) は使用しない、つまり、いきなりネイティブコードを生成します。

ここでは、PC で使われている x86 と x64 について述べます。RISC や IBM などでは、基本的考え方は同じはずですが、Calling Convention (呼び出し規約) がそれぞれ異なるのでいろいろ異なります。

まず、C のソースですが、下のようなものとします。

#include <stdio.h>

void swap(int*, int*);

int main(int argc, char* argv[]) {
  int x = 0;
  int y = 1;

  swap(&x, &y);

  printf("x = %d, y = %d\n", x, y);

  return 0;
}

void swap(int* x, int* y) {
  int u = *x;
  *x = *y;
  *y = u;
}

これのコンパイル結果は、次のようになります。(x86 の場合)

    .file   "Swap.c"
    .section    .rodata
.LC0:
    .string "x = %d, y = %d\n"
    .text
.globl main
    .type   main, @function
main:
    leal    4(%esp), %ecx
    andl    $-16, %esp
    pushl   -4(%ecx)
    pushl   %ebp
    movl    %esp, %ebp
    pushl   %ecx
    subl    $36, %esp
    movl    $0, -8(%ebp) # 0 をスタックフレームのローカルアドレスに保存(x)
    movl    $1, -12(%ebp) # 1 をスタックフレームのローカルアドレスに保存(y)
    leal    -12(%ebp), %eax # y のアドレスを EAX にロード
    movl    %eax, 4(%esp) # EAX をスタックに積む。
    leal    -8(%ebp), %eax # x のアドレスを EAX にロード
    movl    %eax, (%esp) # EAX をスタックに積む。
    call    swap # 関数 swap を呼び出す。
    movl    -12(%ebp), %eax
    movl    -8(%ebp), %edx
    movl    %eax, 8(%esp)
    movl    %edx, 4(%esp)
    movl    $.LC0, (%esp)
    call    printf
    movl    $0, %eax
    addl    $36, %esp
    popl    %ecx
    popl    %ebp
    leal    -4(%ecx), %esp
    ret
    .size   main, .-main
.globl swap
    .type   swap, @function
swap:
    pushl   %ebp
    movl    %esp, %ebp
    subl    $16, %esp
    movl    8(%ebp), %eax # 後から積んだパラメータ(x)を EAX にロード
    movl    (%eax), %eax # EAX の行った先のデータを EAX にロード
    movl    %eax, -4(%ebp) # EAX をローカル変数に保存。
    movl    12(%ebp), %eax # 最初に積んだパラメータ(y)を EAX にロード
    movl    (%eax), %edx # EAX の行った先のデータを EDX にロード
    movl    8(%ebp), %eax # 後から積んだパラメータ(x)を EAX にロード
    movl    %edx, (%eax) # EDX を EAX の行った先に保存
    movl    12(%ebp), %edx # 最初に積んだパラメータ(y)を EDX にロード
    movl    -4(%ebp), %eax # ローカル変数の内容を EAX にロード
    movl    %eax, (%edx) # EAX を EDX の行った先に保存。
    leave
    ret
    .size   swap, .-swap
    .ident  "GCC: (GNU) 4.1.2 20080704 (Red Hat 4.1.2-54)"
    .section    .note.GNU-stack,"",@progbits

x64 の場合は、関数の呼び出し規約が異なっており、整数や浮動小数点数は最適化しなくてもレジスタ渡しになります。

これは、x64 では、汎用レジスタが8本から16本に増えたためで、余ったレジスタを関数のパラメータとして活用しています。

    .file   "Swap.c"
    .section    .rodata
.LC0:
    .string "x = %d, y = %d\n"
    .text
    .globl  main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    subq    $32, %rsp
    movl    %edi, -20(%rbp)
    movq    %rsi, -32(%rbp)
    movl    $0, -8(%rbp)
    movl    $1, -4(%rbp)
    leaq    -4(%rbp), %rdx # RDX には y のアドレスが入る。
    leaq    -8(%rbp), %rax # RAX には x のアドレスが入る。
    movq    %rdx, %rsi # 呼び出し規約に基づいて、パラメータを RSI レジスタ渡しにしている。
    movq    %rax, %rdi # 呼び出し規約に基づいて、パラメータを RDI レジスタ渡しにしている。
    call    swap
    movl    -4(%rbp), %edx
    movl    -8(%rbp), %eax
    movl    %eax, %esi
    movl    $.LC0, %edi
    movl    $0, %eax
    call    printf
    movl    $0, %eax
    leave
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
    .globl  swap
    .type   swap, @function
swap:
.LFB1:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movq    %rdi, -24(%rbp)
    movq    %rsi, -32(%rbp)
    movq    -24(%rbp), %rax
    movl    (%rax), %eax
    movl    %eax, -4(%rbp)
    movq    -32(%rbp), %rax
    movl    (%rax), %edx
    movq    -24(%rbp), %rax
    movl    %edx, (%rax)
    movq    -32(%rbp), %rax
    movl    -4(%rbp), %edx
    movl    %edx, (%rax)
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE1:
    .size   swap, .-swap
    .ident  "GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4"
    .section    .note.GNU-stack,"",@progbits

参照渡しができない言語の場合、言語仕様として参照表現がないというだけで、参照渡しは使われています。

パラメータが値渡しされる場合は、汎用レジスタに値がロードできるもの、具体的には整数や浮動小数点数が値渡しになり、汎用レジスタに値がロードできないもの、具体的には配列や一般のオブジェクトが参照渡しになります。

したがって、参照渡しされる型を利用すれば、swap 関数と同様の機能が実現できます。

つぎのコードは JavaScript で参照渡しと同様の機能を実現する例です。配列は参照渡しなので、swap 関数により2つのパラメータの中身が交換されます。

'use strict';

const swap = (x, y) => {
    let u = x[0];
    x[0] = y[0];
    y[0] = u;
};

var a = [0];
var b = [1];

swap(a, b);

console.log("%i, %i", a, b);