Skip to content

Instantly share code, notes, and snippets.

@sunwu51
Last active June 11, 2022 16:38
Show Gist options
  • Save sunwu51/85e065c69c372dffbb669753984dd9b8 to your computer and use it in GitHub Desktop.
Save sunwu51/85e065c69c372dffbb669753984dd9b8 to your computer and use it in GitHub Desktop.
函数传参与返回值

函数传参的本质

调用函数的地方是实参,函数运行地方是形参,形参是对实参的拷贝

主流编程语言中,下面的函数运行结果都是0,因为形参x是对实参a的拷贝,值类型的拷贝会创建新的内存。这种传参类型也叫值传递。

void main(){
  int a = 0;
  f(a);
  printf("%d", a);//0
}
void f(int x){
  x = 10;
}

image 将参数改为指针类型,指针是变量的地址或者说是变量的引用,传递指针的值也就是传递了变量的引用,这种传递叫引用传递。

void main(){
  int a = 0;
  f(&a);
  printf("%d", a);//10
}
void f(int *x){
  *x = 10;
}

image

在java、js、python等语言中是没有指针的概念的,但是对象变量本身存储的是对象内存的地址,所以可以通过对象进行引用传递。

var obj = {a: 0}
func(obj) // 传递对象实际上就是传递的地址
console.log(obj.a) //10

function func(x){
  x.a = 10;
}

因为只有对象才暗含指针的含义,所以基础数据类型时没办法实现在函数中修改自己的值的,此外如果想通过函数修改一个对象的地址也是做不到的,这在c++中需要用双指针,或者指针的引用才能做到。

在golang中,使用了指针和地址,与C语言非常接近, *int表示int指针类型,&a是对a取地址。

func main(){
  a:=0
  f(&a)
  println(a) //10
}
func f(x *int){
  *x = 10
}

改变结构体的值,注意golang中的语法糖 (*x).age可以直接写成x.age,golang自动判断如果是指针取属性,就自动换成指针指向的结构体取属性

type user struct{
    age int
}
func main(){
  a:=user{0}
  f(&a)
  println(a.age) // 10
}
func f(x *user){
    // x.age = 10  这两种方法都可改变a.age但是原理不同,这一行是在原地址上进行修改age这一个属性
    *x = user{10}  //这一行是直接将a内存中的整个数据换成新的
}

C++的引用类型int &x是指针的语法糖,可以简化使用指针的写法,如下

void main(){
  int a = 0;
  f(a);
  printf("%d", a);//10
}
void f(int &x){
  x = 10;
}

引用暗含一层指针,那么指针的引用则可以实现双指针效果:

void changePerson(Person* p1, Person* &p2, Person** p3){
  p1->age ++;
  p2->age ++;
  (*p3)->age ++;
    
}
void changePerson2(Person* p1, Person* &p2, Person** p3){
  p1 = new Person(11, "11"); // 没用
  p2 = new Person(22, "22");
  *p3 = new Person(33, "33");
}
int main()
{
  Person *p1 = new Person(1,"1");
  Person *p2 = new Person(2,"2");
  Person *p3 = new Person(3,"3");
  printf("p1 %p, p2 %p, p3 %p\n", p1, p2, p3);
  changePerson(p1, p2, &p3);
  printf("p1 %p, p2 %p, p3 %p\n", p1, p2, p3);
  printf("p1 %d, p2 %d, p3 %d\n", p1->age, p2->age, p3->age);
  changePerson2(p1, p2, &p3);
  printf("p1 %p, p2 %p, p3 %p\n", p1, p2, p3);
  printf("p1 %d, p2 %d, p3 %d\n", p1->age, p2->age, p3->age);
  //delete(p1);生产环境记得清理内存
}
/*
打印
p1 0x55e9cfc03eb0, p2 0x55e9cfc03ee0, p3 0x55e9cfc03f10
p1 0x55e9cfc03eb0, p2 0x55e9cfc03ee0, p3 0x55e9cfc03f10
p1 2, p2 3, p3 4
p1 0x55e9cfc03eb0, p2 0x55e9cfc04380, p3 0x55e9cfc043b0
p1 2, p2 22, p3 33

对象内部属性的改变,单指针就可以完成,更不用说其他两个
而直接改变指针内的地址,则需要双指针或者指针的引用
*/

rust也是面向过程的编程语言,同样是struct和其他基础类型都是值类型。他没有地址和指针,但有引用的概念,引用作为函数参数又叫做借用。

rust有一些设计原则:

  • 一个变量只能有一个owner,但是可以引用该变量,和借用变量放入函数中,引用与借用本质是内存地址
  • 可变引用在一个作用范围只能有一个

函数返回值

新的函数调用栈开辟的时候和父函数中间会有一定的空间来接收函数的返回值。对于返回值和入参一样,同样有值类型和引用类型的传递,如果是值类型传递例如int,那就是将返回值的值直接拷贝给父函数中接收返回值的变量。如果是返回的地址,那么地址值同样能够通过*下钻取到地址内的数据值,这就是引用传递。

如果返回的地址是函数本地变量的地址,那么函数完成后,栈会退出,栈的退出不会删除内存的数据,而是把指向栈顶的指针缩回去。新的其他函数调用从栈顶开始,就能自动覆盖内存。

下面例子就是返回本地变量地址,编译会收到警告warning: function returns address of local variable [-Wreturn-local-addr],运行会报错Segmentation fault (core dumped),因为printf函数覆盖了f函数的栈空间,导致不可预期的异常。

#include <stdio.h>
int* f(){
  int x = 1;
  return &x;
}
int main(){
  printf("%d", *(f()));
  return 0;
}
//Segmentation fault (core dumped)

原则上如果引用类型的地址作为返回值,就需要使用堆内存的空间。

#include <stdio.h>
#include <stdlib.h>
int* f(){
    int* x = malloc(sizeof(int)); // malloc是申请堆内存
    *x = 99;
    return x;
}

int main()
{
    int *p = f();
    printf("%d", *p); // 打印99
    free(p);          // 需要手动清理堆内存
    return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment