LoginSignup
2
3

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-11-30

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

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

ただ、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);

2
3
44

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3