一文助你搞懂参数传递原理解析(java、go、python、c++)

前言

最近一年多的时间陆续接触了一些对我来说陌生的语言,主要就是 PythonGo,期间为了快速实现需求只是依葫芦画瓢的撸代码;并没有深究一些细节与原理。

就拿参数传递一事来说各个语言的实现细节各不相同,但又有类似之处;在许多新手入门时容易搞不清楚,导致犯一些低级错误。

Java

基本类型传递

先拿我最熟悉的 Java 来说,我相信应该没人会写这样的代码:

	@Test
 public void testBasic() {
 int a = 10;
 modifyBasic(a);
 System.out.println(String.format("最终结果 main a==%s", a));
 }

 private void modifyBasic(int aa) {
 System.out.println(String.format("修改之前 aa==%s", aa));
 aa = 20;
 System.out.println(String.format("修改之后 aa==%s", aa));
 }

输出结果:

修改之前 aa==10
修改之后 aa==20
最终结果 main a==10

不过从这段代码的目的来看应该是想要修改 a 的值,从直觉上来说如果修改成功也是能理解的。

至于结果与预期不符合的根本原因是理解错了参数的值传递与引用传递。

在这之前还是先明确下值传递与引用传递的区别:

这里咱们先抛出结论,Java 采用的是值传递;这样也能解释为什么上文的例子没有成功修改原始数据。

参考下图更好理解:

当发生函数调用的时候 a 将自己传入到 modifyBasic 方法中,同时将自己的值复制了一份并赋值给了一个新变量 aa 从图中可以看出这是 aaa 两个变量没有一毛钱关系,所以对 aa 的修改并不会影响到 a

有点类似于我把苹果给了老婆,她把苹果削好了;但我手里这颗并没有变化,因为她只是从餐盘里拿了一颗一模一样的苹果削好了。

如果我想要她那颗,只能让她把削好的苹果给我;也就类似于使用方法的返回值。

a = modifyBasic(a);

引用类型传递

下面来看看引用类型的传递:

 private class Car{
 private String name;

 public Car(String name) {
 this.name = name;
 }

 @Override
 public String toString() {
 return "Car{" +
 "name='" + name + '\'' +
 '}';
 }
 }

		@Test
 public void test01(){
 Car car1 = new Car("benz");
 modifyCar1(car1);
 System.out.println(String.format("最终结果 main car1==%s", car1));
 }

 private void modifyCar1(Car car){
 System.out.println(String.format("修改之前 car==%s", car));
 car.name = "bwm";
 System.out.println(String.format("修改之后 car==%s", car));
 }

在这个例子里先创建了一个 benzcar1,通过一个方法修改为 bmw 那最开始的 car1 会受到影响嘛?

修改之前 car==Car{name='benz'}
修改之后 car==Car{name='bwm'}
最终结果 main car1==Car{name='bwm'}

结果可能会与部分人预期相反,这样的修改却是可以影响到原有数据的?这岂不是和值传递不符,看样子这是引用传递吧?

别急,通过下图分析后大家就能明白:

test01 方法中我们创建了一个 car1 的对象,该对象存放于堆内存中,假设内存地址为 0x1102 ,于是 car1 这个变量便应用了这块内存地址。

当我们调用 modifyCar1 这个方法的时候会在该方法栈中创建一个变量 car ,接下来重点到了:

这个 car 变量是由原本的入参 car1 复制而来,所以它所对应的堆内存依然是 0x1102

所以当我们通过 car 这个变量修改了数据后,本质上修改的是同一块堆内存中的数据。从而原本引用了这块内存地址的 car1 也能查看到对应的变化。

这里理解起来可能会比较绕,但我们记住一点就行:

传递引用类型的数据时,传递的并不是引用本身,依然是值;只是这个 是内存地址罢了。

因为把相同的内存地址传过去了,所以对数据的操作依然会影响到外部。

所以同理,类似于这样的代码也会影响到外部原始数据:

@Test
 public void testList(){
 List<Integer> list = new ArrayList<>();
 list.add(1);
 addList(list);
 System.out.println(list);
 }

 private void addList(List<Integer> list) {
 list.add(2);
 }

 [1, 2]

那如果是这样的代码:

@Test
 public void test02(){
 Car car1 = new Car("benz");
 modifyCar(car1);
 System.out.println(String.format("最终结果 main car1==%s", car1));
 }

 private void modifyCar(Car car2) {
 System.out.println(String.format("修改之前 car2==%s", car2));
 car2 = new Car("bmw");
 System.out.println(String.format("修改之后 car2==%s", car2));
 }

假设 Java 是引用传递那最终的结果应该是打印 bmw 才对。

修改之前 car2==Car{name='benz'}
修改之后 car2==Car{name='bmw'}
最终结果 main car1==Car{name='benz'}

从结果又能佐证这里依然是值传递。

如果是引用传递,原本的 0x1102 应该是被直接替换为新创建的 0x1103 才对;而实际情况如上图所示,car2 直接重新引用了一个对象,两个对象之间互不干扰。

Go

相对于 Java 来说 Go 的用法又有所不同,不过我们也可以先得出结论:

Go语言的参数也是值传递。

Go 语言中数据类型主要有以下两种:

值类型与引用类型;

值类型

先以值类型举例:

func main() {
	a :=10
	modifyValue(a)
	fmt.Printf("最终 a=%v", a)
}

func modifyValue(a int) {
	a = 20
}
输出:最终 a=10

函数调用过程与之前的 Java 类似,本质上传递到函数中的值也是 a 的拷贝,所以对其的修改不会影响到原始数据。

当我们把代码稍加修改:

func main() {
	a :=10
	fmt.Printf("传递之前a的内存地址%p \n", &a)
	modifyValue(&a)
	fmt.Printf("最终 a=%v", a)
}

func modifyValue(a *int) {
	fmt.Printf("传递之后a的内存地址%p \n", &a)
	*a = 20
}

传递之前a的内存地址0xc0000b4040
传递之后a的内存地址0xc0000ae020
最终 a=20

从结果来看最终 a 的值是被方法修改了,这点便是 GoJava 很大的不同点:

Go 中存在着指针的概念,我们可以将变量通过指针的方式传递到不同的方法中,在方法里便可通过这个指针访问甚至修改原始数据。

那这么一看不就是引用传递嘛?

其实不然,我们仔细看看刚才的输出会发现参数传递前后的内存地址并不相同。

传递之前a的内存地址0xc0000b4040
传递之后a的内存地址0xc0000ae020

这也恰好论证了值传递,因为这里实际传递的是指针的拷贝。

也就是说 modifyValue 方法中的参数与入参的&a都是同一块内存的指针,但指针本身也是需要内存来存放的,所以在方法调用过程中新建了一个指针 a ,从而导致他们的内存地址不同。

虽然内存地址不同,但指向的数据都是同一块,所以方法内修改后原始数据也受到了影响。

引用类型

对于 map slice channel 这类引用类型又略有不同:

func main() {
	var personList = []string{"张三","李四"}
	modifySlice(personList)
	fmt.Printf("slice=%v \n", personList)
}
func modifySlice(personList []string) {
	personList[1] = "王五"
}

slice=[张三 王五]

最终我们会发现原始数据也被修改了,但我们并没有传递指针;同样的特性也适用于 map

但其实我们查看 slice 的源码会发现存放数据的 array 就是指针类型:

type slice struct {
	array unsafe.Pointer
	len int
	cap int
}

所以我们可以直接对数据进行修改,相当于间接的带了指针。

使用建议

那我们在什么时候使用指针呢?有以下几点建议:

如果参数是基本的值类型,比如 int,float 建议直接传值。如果需要修改基本的值类型,那只能是指针;但考虑到代码可读性还是建议将修改后的值返回用于重新赋值。数据量较大时建议使用指针,减少不必要的值拷贝。(具体多大可以自行判断)

Python

Python 中变量是否可变是影响参数传递的重要因素:

如上图所示,bool int float 这些不可变类型在参数传递过程中是不能修改原始数据的。

if __name__ == '__main__':
		x = 1
 modify(x)
 print('最终 x={}'.format(x))	

def modify(val):
 val = 2

最终 x=1

原理与 Java Go中类似,是基于值传递的,这里就不再复述。

这里重点看看可变数据类型在参数传递中的过程:

if __name__ == '__main__':
		x = [1]
 modify(x)
 print('最终 x={}'.format(x))	

def modify(val):
 val.append(2)

最终 x=[1, 2]

最终数据受到了影响,那么就表明这是引用传递嘛?再看个例子试试:

if __name__ == '__main__':
		x = [1]
 modify(x)
 print('最终 x={}'.format(x))	

def modify(val):
 val = [1, 2, 3]

最终 x=[1]

显而易见这并不是引用传递,如果是引用传递最终 x 应当等于 [1, 2 ,3]

从结果来看这个传递过程非常类似 Go 中的指针传递,val 拿到的也是 x 这个参数内存地址的拷贝;他们都指向了同一块内存地址。

所以对这块数据的修改本质上改的是同一份数据,但一旦重新赋值就会创建一块新的内存从而不会影响到原始数据。

Java 中的上图类似。

所以总结下:

  • 对于不可变数据:在参数传递时传递的是值,对参数的修改不会影响到原有数据。
  • 对于可变数据:传递的是内存地址的拷贝,对参数的操作会影响到原始数据。

这么说来这三种都是值传递了,那有没有引用传递的语言呢?

当然,C++是支持引用传递的:

#include <iostream>
using namespace std;

class Box
{
 public:
 double len;
};

void modify(Box& b);

int main ()
{
	Box b1;
	b1.len=100;
	cout << "调用前,b1 的值:" << b1.len << endl;
	modify(b1);
	cout << "调用后,b1 的值:" << b1.len << endl;
	return 0;
}

void modify(Box& b)
{
	b.len=10.0;
	Box b2;
	b2.len = 999;
	b = b2;

	return;
}

调用前,b1 的值:100
调用后,b1 的值:999

可以看到把新对象 b2 赋值给入参 b 后是会影响到原有数据的。

总结

其实这几种语言看下来会发现他们中也有许多相似之处,所以通常我们在掌握一门语言后也能快速学习其他语言。

但往往是这些基础中的基础最让人忽略,希望大家在日常编码时能够考虑到这些基础知识多想想一定会写出更漂亮的代码(bug)。

到此这篇关于多种语言参数传递原理解析(java、go、python、c++)的文章就介绍到这了,更多相关java参数传递原理内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 简单谈谈Java中String类型的参数传递问题

    提要:本文从实现原理的角度上阐述和剖析了:在Java语言中,以 String 作为类型的变量在作为方法参数时所表现出的"非对象"的特性. 一.最开始的示例 写代码最重要的就是实践,不经过反复试验而得出的说辞只能说是凭空遐想罢了.所以,在本文中首先以一个简单示例来抛出核心话题: public class StringAsParamOfMethodDemo { public static void main(String[] args) { StringAsParamOfMethodDem

  • java 中函数的参数传递详细介绍

    java中函数的参数传递 总结: 1.将对象(对象的引用)作为参数传递时传递的是引用(相当于指针).也就是说函数内对参数所做的修改会影响原来的对象.   2.当将基本类型或基本类型的包装集作为参数传递时,传递的是值.也就是说函数内对参数所做的修改不会影响原来的变量.   3.数组(数组引用))作为参数传递时传递的是引用(相当于指针).也就是说函数内对参数所做的修改会影响原来的数组.   4.String类型(引用)作为参数传递时传递的是引用,只是对String做出任何修改时有一个新的String

  • Django中URL的参数传递的实现

    在Django中有非常强大的URL模块,可以按照开发者的想法来制定清晰的URL,同时支持正则表达式.此外,在URL中还可以传递参数. 1. Django处理请求的方式 1) Django通过URLconf模块来进行判断.通常情况下,这就是ROOT_URLCONF配置的价值,但是如果请求携带了一个urlconf的属性(通常被中间件设置),那么这个被携带的urlconf将会替代ROOT_URLCONF的配置. 2) Django会调用Python模块并寻找各种urlpatterns.这是一个属于dj

  • Python def函数的定义、使用及参数传递实现代码

    Python编程中对于某些需要重复调用的程序,可以使用函数进行定义,基本形式为: def 函数名(参数1, 参数2, --, 参数N): 执行语句函数名为调用的表示名,参数则是传入的参数,可以更具需要定义,也可以没有. # 例1:简单的函数使用 # coding=gb2312 # 定义函数 def hello(): print 'hello python!' # 调用函数 hello() >>> hello python! 函数可以带参数和返回值,参数将按从左到右的匹配,参数可设置默认值

  • python 类之间的参数传递方式

    练手记录以及调试步骤. class A(object): def __init__(self,a="A"): print("enter",a) print("leave",a) def mainA(readA,*D): #如果使用实例化B(),调用mainA,D收集进来D多余的参数.(参考第3点说明) print("enter mainA") reaA = "内部添加的" print("leave

  • Java的数据类型和参数传递(详解)

    Java提供的数据类型主要分为两大类:基本数据类型和引用数据类型. Java中的基本数据类型 名称 大小 取值范围 byte型 (字节) 8bit -128-127 (-2^7到2^7-1) short型 (短整型) 16bit -2^15到2^15-1 int型 (整形) 32bit -2^31到2^31-1 long型 (长整型) 64bit -2^63到2^63-1 float型 (单精度浮点型) 32bit double型 (双精度浮点型) 64bit char型 (字符型) 16bit

  • C++ 二维数组参数传递的实现方法

    C++ 二维数组参数传递的实现方法 int a[2][2]={ {4, 6}, {9, 11} }; 我定义了这样一个数组,我想把这个数组作为参数,传递到一个函数中,并要在函数中能引用该二维数组的元素,我该怎么做? 第一种方式是直接传递二维数组,但是必须标明第二维的值,因为如果只是传递a[][],编译器无法分配这样的数组,所以要这样传int a[][3] 第二种方法是传递指针数组方式,即int (*a)[3] 第三种是传递指针方法. 具体实施见代码: 方法1:传递数组,注意第二维必须标明 //二

  • django中的HTML控件及参数传递方法

    本文对djangoHTML的表单控件中的单选及多选进行介绍,并说明如何进行参数传递. 1.HTML中的表单控件: 在HTML中表单的书写一般为: <form method="post" action=""> 这个method代表方法,方法一般有两个一个是'post',一个是'get',action是提交表单到何处,可填写一个网址.不填则默认到本页面. {%csrf_token%} 这个是django中的一个标签,用于防止恶意攻击使用,如果不加入这个标签,

  • 一文助你搞懂参数传递原理解析(java、go、python、c++)

    前言 最近一年多的时间陆续接触了一些对我来说陌生的语言,主要就是 Python 和 Go,期间为了快速实现需求只是依葫芦画瓢的撸代码:并没有深究一些细节与原理. 就拿参数传递一事来说各个语言的实现细节各不相同,但又有类似之处:在许多新手入门时容易搞不清楚,导致犯一些低级错误. Java 基本类型传递 先拿我最熟悉的 Java 来说,我相信应该没人会写这样的代码: @Test public void testBasic() { int a = 10; modifyBasic(a); System.

  • 一文带你搞懂Java中Synchronized和Lock的原理与使用

    目录 1.Synchronized与Lock对比 2.Synchronized与Lock原理 2.1 Synchronized原理 2.2 Lock原理 3.Synchronized与Lock使用 Synchronized Lock 4.相关问题 1.Synchronized与Lock对比 实现方式:Synchronized是Java语言内置的关键字,而Lock是一个Java接口. 锁的获取和释放:Synchronized是隐式获取和释放锁,由Java虚拟机自动完成:而Lock需要显式地调用lo

  • 一文带你搞懂Numpy中的深拷贝和浅拷贝

    目录 1. 引言 2. 浅拷贝 2.1 问题引入 2.2 问题剖析 3. 深拷贝 3.1 举个栗子 3.2 探究原因 4. 技巧总结 4.1 判断是否指向同一内存 4.2 其他数据类型 5. 总结 1. 引言 深拷贝和浅拷贝是Python中重要的概念,本文重点介绍在NumPy中深拷贝和浅拷贝相关操作的定义和背后的原理. 闲话少说,我们直接开始吧! 2. 浅拷贝 2.1 问题引入 我们来举个栗子,如下所示我们有两个数组a和b,样例代码如下: import numpy as np a = np.ar

  • 一文搞懂Codec2框架解析

    目录 1 前言–Codec2.0是什么 2 Codec2.0框架 3 流程解析 3.1 初始化流程 3.2 启动流程 3.3 Input Buffer的回调 3.4 Output Buffer的回调 4 总结 1 前言–Codec2.0是什么 在Android Q之前,Android的两套多媒体框架分别为MediaPlayer与MediaCodec,后者只负责解码与渲染工作,解封装工作由MediaExtractor代劳,MediaCodec经由ACodec层调用第三方编解码标准接口OpenMAX

  • 一文带你搞懂JS中六种For循环的使用

    目录 一.各个 for 介绍 1.for 2.for ... in 3.for ... of 4.for await...of 5.forEach 6.map 二.多个 for 之间区别 1.使用场景差异 2.功能差异 3.性能差异 三.for 的使用 for 循环在平时开发中使用频率最高的,前后端数据交互时,常见的数据类型就是数组和对象,处理对象和数组时经常使用到 for 遍历,因此下班前花费几分钟彻底搞懂这 5 种 for 循环.它们分别为: for for ... in for ... o

  • 一文带你搞懂Maven的继承与聚合

    目录 一.继承 二.继承关系实施步骤 三.聚合与继承的区别 一.继承 我们已经完成了使用聚合工程去管理项目,聚合工程进行某一个构建操作,其他被其管理的项目也会 执行相同的构建操作.那么接下来,我们再来分析下,多模块开发存在的另外一个问题,重复配置的问题,我们先来看张图: ■ spring-webmvc.spring-jdbc在三个项目模块中都有出现,这样就出现了重复的内容 ■ spring-test只在ssm_crm和ssm_goods中出现,而在ssm_order中没有,这里是部分重复的内容

  • 一文带你搞懂Spring响应式编程

    目录 1. 前言 1.1 常用函数式编程 1.2 Stream操作 2. Java响应式编程 带有中间处理器的响应式流 3. Reactor 3.1 Flux & Mono 3.2 Flux Mono创建与使用 4. WebFlux Spring WebFlux示例 基于注解的WebFlux 基于函数式编程的WebFlux Flux与Mono的响应式编程延迟示例 总结 哈喽,大家好,我是指北君. 相信响应式编程经常会在各种地方被提到.本篇就为大家从函数式编程一直到Spring WeFlux做一次

  • 一文带你搞懂Java中的泛型和通配符

    目录 概述 泛型介绍和使用 泛型类 泛型方法 类型变量的限定 通配符使用 无边界通配符 通配符上界 通配符下界 概述 泛型机制在项目中一直都在使用,比如在集合中ArrayList<String, String>, Map<String,String>等,不仅如此,很多源码中都用到了泛型机制,所以深入学习了解泛型相关机制对于源码阅读以及自己代码编写有很大的帮助.但是里面很多的机制和特性一直没有明白,特别是通配符这块,对于通配符上界.下界每次用每次百度,经常忘记,这次我就做一个总结,加

  • 一文带你搞懂PHP对象注入

    目录 背景 漏洞案例 PHP类和对象 php magic方法 php对象序列化 序列化magic函数 php对象注入 常见的注入点 其他的利用方法 如何利用或者避免这个漏洞 结论 背景 php对象注入是一个非常常见的漏洞,这个类型的漏洞虽然有些难以利用,但仍旧非常危险,为了理解这个漏洞,请读者具备基础的php知识. 漏洞案例 如果你觉得这是个渣渣洞,那么请看一眼这个列表,一些被审计狗挖到过该漏洞的系统,你可以发现都是一些耳熟能详的玩意(就国外来说) WordPress 3.6.1 Magento

  • 一文带你搞懂Golang结构体内存布局

    目录 前言 结构体内存布局 结构体大小 内存对齐 总结 前言 结构体在Go语言中是一个很重要的部分,在项目中会经常用到,大家在写Go时有没有注意过,一个struct所占的空间不一定等于各个字段加起来的空间之和,甚至有时候把字段的顺序调整一下,struct的所占空间不一样,接下来通过这篇文章来看一下结构体在内存中是怎么分布的?通过对内存布局的了解,可以帮助我们写出更优质的代码.感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助. 结构体内存布局 结构体大小 结构体实际上就是由各种类型的数据组合而成

随机推荐