##関数パラメータの参照渡しとはどういうことなのか?
関数呼び出しで、関数(メソッド)のパラメータの渡し方には「値渡し」と「参照渡し」というのがあります。
ただ、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);
–