真正搞懂传值引用和传指针引用

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u013687632/article/details/78616495

通过一个基本原则和两个例子讲述传值引用和传指针引用的区别,并且从系统底层解析这两种调用的区别

  • acmore
  • 2017.11.14

1. 问题概述

在C语言调用函数时,有两种传参数的方法,一种是传值,另一种是传参数。对于C语言的初学者(甚至是一些C语言的熟练使用者)而言,区别这两种形式往往比较困难,再加上C语言有指针,以及指针的指针等等,复杂多变的形式会使得这个过程更加困难。

很多初学者会尝试通过形式来记忆这两种方式,比如函数声明中有*的就是传指针,没有的就是传值,但是如果传入的参数本身就是一个指针,无论是传值还是传指针,函数声明都会有*,这种时候原有的形式记忆不再有效,需要再次记忆另外一套形式。形式是无穷无尽的,只有明白指针类型的本质,以及在函数调用过程中系统里到底发生了什么,才能真正理解这两种方式。

2. 简单认识

现在我们有两个函数,作用都是修改一个int类型的值,分别如下:

#include <stdio.h>

void modify1(int a) {
    a = 10086;
}

void modify2(int *a) {
    *a = 10086;
}

int main() {
    int x = 1;
    printf("%d\n", x);
    modify1(x);
    printf("%d\n", x);

    int y = 1;
    printf("%d\n", y);
    modify2(&y);
    printf("%d\n", y);

    return 0;
}

运行程序,会输出什么呢?

1
1
1
10086

这个程序很简单,我们可以轻易看出modify1是传值引用,modify2是传指针引用。传值引用不会改变调用者的值,而传指针会改变调用者的值,并且在形式上,函数声明中有*,函数调用有&。因此我们这样记忆:只要改变了调用者的值,并且有*的函数就是传指针引用。

真的是这样吗?

3. 深入理解

下面我们定义一个结构体,这个结构体含有两个整数值,然后类似地写出两个修改函数:

#include <stdio.h>
#include <malloc.h>

typedef struct Value {
    int a;
    int b;
}Value;

void modify1(Value *v) {
    v->a = 10086;
    v->b = 10010;
}

void modify2(Value **v) {
    (*v)->a = 10086;
    (*v)->b = 10010;
}

int main() {
    Value *v1 = (Value *) malloc(sizeof(Value));
    v1->a = v1->b = 1;
    printf("%d\t%d\n", v1->a, v1->b);
    modify1(v1);
    printf("%d\t%d\n", v1->a, v1->b);

    Value *v2 = (Value *) malloc(sizeof(Value));
    v2->a = v2->b = 1;
    printf("%d\t%d\n", v2->a, v2->b);
    modify2(&v2);
    printf("%d\t%d\n", v2->a, v2->b);

    return 0;
}

输出结果如下:

1   1
10086   10010
1   1
10086   10010

这次我们再按照之前的经验去判断,好像有点行不通,因为在对modify1modify2的调用上虽然像是一个传值一个传指针,但是两个函数的声明中都有*,并且v1v2的值都被改变了,经验不再适用!

这种时候我们应该回归底层,老老实实从系统层面理解在程序执行过程中到底发生了什么,我们首先应该记住的一个最重要的原则是:
- 指针类型和基本类型一样,本质没有什么区别

我们拿类型intValue *做比较,根据上述原则,便有以下几条原则:

  1. int在内存中占有一块空间,那么Value *同样会在内存中占有一块空间

  2. 可以得到一个指针指向int,那么也可以有一个指针指向Value *

  3. 使用&可以取得int的指针,那么同样可以使用&取得Value *的指针

  4. 传值引用时,有变量int aValue* v,下边两种写法都是传值引用

    • 操作int时,函数的参数类型就是int,调用函数时传入a
    • 同理在操作Value *时,函数的参数类型就是Value *,调用函数时传入v
  5. 传指针引用时,下边两种写法都是传指针引用
    • 操作int时,函数的参数类型是int *,调用函数时传入&a
    • 操作Value *时,函数的参数类型是Value **,调用函数时传入&v

根据以上原则,不要把指针当做一个特殊的类型,把指针类型那一堆当成一个整体,传值时直接丢进去,传指针时就在后边加一个*,在对应的调用前加上&即可。

4. 问题拓展

现在我们要完成一个这样的函数:它可以代替malloc函数来对我们刚刚声明的一个指针进行赋值,在第三部分中我们了解到,在操作指针类型时,传值引用一样可以改变调用者的值,所以我们写出了下边的程序:

#include <stdio.h>
#include <malloc.h>

typedef struct Value {
    int a;
    int b;
}Value;

void create(Value *v) {
    v = (Value *) malloc(sizeof(Value));
    v->a = 10086;
    v->b = 10010;
}

int main() {
    Value *v1;
    create(v1);
    printf("%d\t%d", v1->a, v1->b);
    return 0;
}

结果输出如下:

-16219251   -970326017

并没有符合我们的期待!说好的操作指针就可以高枕无忧了呢?这个时候我们需要从系统底层了解传值和传指针时到底发生了什么,如下图所示

传值引用时程序地址空间图示

首先v1的值是(p1p2p3p4p5p6p7p8)(32位系统为例),当调用create函数时,就是图上的①过程,这个时候v1的值被拷贝到v中,这时候如果直接对v操作是没有问题的,可以改变v1的值,因为它们俩指向的是同一块内存空间(事实上此时还没有指向任何有意义的空间),但是在随后的赋值语句中,即②过程,v的值立刻被替换为(k1k2k3k4k5k6k7k8),之后对v进行的操作其实是对(k1k2k3k4k5k6k7k8)指向的空间的操作,这时候根本没v,也就是(p1p2p3p4p5p6p7p8)指向的空间什么事,因此不能达到我们想要的效果。如果想要完成修改,仍需要传指针引用,修改后的代码如下:

#include <stdio.h>
#include <malloc.h>

typedef struct Value {
    int a;
    int b;
}Value;

void create(Value **v) {
    (*v) = (Value *) malloc(sizeof(Value));
    (*v)->a = 10086;
    (*v)->b = 10010;
}

int main() {
    Value *v1;
    create(&v1);
    printf("%d\t%d", v1->a, v1->b);
    return 0;
}

5. 最后总结

  1. 指针没有什么特殊的,基本数据类型怎么对待,指针就怎么对待
  2. 要理解函数调用过程中进程地址空间中的变化,它能直观地体现传值引用和传指针引用的区别,并解决在使用过程中出现的问题
  3. 不要死记硬背形式,任何问题尝试回归底层理解

没有更多推荐了,返回首页