• [编程开发] 由指针传參引发的一点分析


    昨天有同学(初学指针)在练习单链表和二叉树的时候,程序老是崩溃,或者得不到正确结果,于是向我求助。问题就出在指针的參数传递上,没传好指针导致内存混乱,其它代码基本全对。这个错误十分可惜。故在此我想做个记录,可能显得十分基础。

    假设函数的參数是普通的一级指针,那么就意味着你仅仅能使用指针、改变指针指向或者改变指向的目标变量。不能试图通过这个指针来申请内存。

    void getMemory(int *p)
    {
    	p = (int *)malloc(sizeof(int) * 10);
    }
    
    void func()
    {
    	int *num = NULL;
    	getMemory(num);
    	// 指针p依然是NULL指针
    }
    这是一个很常见的错误。当发生函数调用的时候,函数的形參总是实參的一个copy(副本),就算是指针也是如此。比如上述的代码能够看成:在调用函数的时候,把num指向的目标地址赋值给p指针。如今num指针和p指针为两个全然不同的实体,它们同一时候指向了同一个目标地址。通过以下的代码能够验证这个说法。
    #include <iostream>
    using namespace std;
    
    void getMemory(int *p)
    {
      printf("target address of pointer p is: %x, its value is: %x
    ", p, *p);
      printf("but address of pointer p itself is: [%x]
    ", &p);
    }
    
    
    int main()
    {
        int val = 4;
        printf("address of val is: %x
    ", &val);
        int *num = &val;
        printf("target address of pointer num is:%x, its value is:%x
    ", num, *num);
        printf("address of pointer num itself is: [%x]
    ", &num);
        getMemory(num);
    
        // printf("%d %d %d
    ", *p, *(p+1), *(p+2));
    
        getchar();
        return 0;
    }
    这样一来的话,形參上的p指针是一个新的指针,它的指向和原来的num指针一样,所以能够正常的改动目标地址的变量和进行输出。

    如今函数中的p指针申请了新的内存,如今就非常明显了:它的这个行为和原来的指针没有一点关系,那是p指针自己的事情。

    解决这个错误的方法有非常多,事实上怎么做都行,仅仅要搞清楚申请过来的内存究竟是给谁用的即可了。

    方法一:利用指向指针的指针

    void getMemory(int **p)
    {
    	*p = (int *)malloc(sizeof(int) * 10);
    }
    
    void func()
    {
    	int *num = NULL;
    	getMemory(&num);
    }
    为什么这样能够呢?我们来分析一下。**p是一个指向指针的指针,前面说过,num和p指向了同一个目标,那么num和p都能读取或是改动目标。

    既然这种话,那我直接拿实參,也就是num指针,作为我的目标。

    假设你了解&p, p, *p的话,int **p事实上一点也不神奇。还记得int *p中的int是什么意思吗?既然它是指向指针的指针,那么理所应该就应该这样写了:int* *p,由于它自身是指针,并且指向的是一个int*类型的指针。只是我非常不建议写成int* *p,毕竟二级指针和一级指针非常有不同,为了突出二级指针应该写成:int **p(有些人写成int** p)。

    全然不须要搞得那么混乱,二级指针p的目标是个指针,仅此而已。操作二级指针的目标,即*p,就相当于在操作指针num,即*p == num。*num是什么意思呢?在num前面加上了个*,表示想要操作num指针所指向的对象,非常遗憾上图中num指向的是NULL,不太好说明。又由于num == *p,所以*num == *(*p) == **p。

    相信看了上图你应该对*p和**p非常明白了。

    好了,回过头来看看代码:

    *p = (int *)malloc(sizeof(int) * 10);
    *p指向的是num,也就是num自身。如今让num自己去申请内存,而不是让别人代为申请,当然就正确了:)

    方法二:直接申请一块内存,然后把地址返回给指针

    申请内存事实上非常easy,我们通常这样写:

    int *pBuffer = (int *)malloc(sizeof(int) * 10);
    意思是说,让操作系统给我安排一块位置,然后把这块位置的地址告诉我。就是把这块新内存的地址返回给pBuffer这个指针,这样pBuffer指向这块内存以后就能够进行操作了。既然这种话,那我们就让num指针直接做这件事情,代码例如以下:

    int* getMemory()
    {
    	int *p = (int *)malloc(sizeof(int) * 10);
    	return p;
    }
    
    void func()
    {
    	int *num = NULL;
    	num = getMemory();
    }
    这段代码应该非常easy理解。函数里p申请了一块内存,然后p指向了这快内存,最后p把新内存的地址返回给了num指针。这是安全的,前面说过,p指针自身是函数的局部变量,存放于栈中,但p指针申请来的内存是存放在堆中的,所以函数结束后p会被释放,但这内存块不会。假设内存块也存在栈中那就不行了。




    知道了上述原理依然是不够的,我们再来看看例如以下代码:
    char* getString()
    {
    	char *s = "hactrox";
    	return s;
    }
    
    void func()
    {
    	char *str = NULL;
    	str = getString();
    }

    getString函数中的字符串"hactrox"存放在文字常量区,顾名思义,既然是"常量"区,肯定是仅仅读的,那么换句话说不论什么对这个字符串的改动都是不同意的。

    上述代码尽管是全然正确的,但同一时候也埋下了不小的隐患。仅仅要str指针在不论什么时候试图改动这个字符串,就会导致程序奔溃。

    方法三:使用引用

    首先要说明一下引用是什么概念。C语言没有引用的概念,&符号仅仅作为取地址用。引用是C++里的概念,非常easy就把引用和指针搞混了。引用就是别名。

    先来看一个普通到不能再普通的语句:

    int value = 10;
    这个int型的变量value,显然不是指针,它就是个变量名,代表了这块内存的名字。

    int *p = &value;
    int &nickname = num;
    第二行行语句的意思是说,我创建了一个新的东西,这个东西是num变量的别名,nickname这个东西既不是变量也不是指针也不是副本,更不是字符串,那它总得有个名字吧?就叫它引用好了。引用和指针的差别是,指针自身就是个实体,value手中掌握着10这个数字,指针指向了value手中的数字,他们的"共同目标"是这个数字10。而引用则全然不一样,对引用来说,根本没有"共同目标"这一说法,由于引用本身就是value他自身,是value这个变量的还有一个名字。

    引用和指针的一些差别例如以下:

    1. 能够先创建指针,然后再指向一个目标。而引用在创建的时候就必须指定目标。总得现有这个人,然后这个人才有昵称吧。

    2. 相对于引用来说,指针很自由,指针能够指向一个目标也能够指向NULL。可是却不能有NULL引用。这不仅违背了引用的设计初衷,并且逻辑上也说只是去。一个事物本身就不存在了,哪来的昵称?就算能有昵称的话,那它本来的名字叫什么?

    3. 一旦为一个变量设立引用以后,这个引用就和这个变量绑定了。换句话说,就是这个引用就不能指向别的变量上。所以引用就相当于变量的属性。试想,给一个人取了外号以后,总不可能用这个外号去称呼还有一个人吧?假设是这种话,那么肯定有非常多人搞不清楚究竟谁叫这个外号,系统也是。所以当然就不行了。

    #include <iostream>
    using namespace std;
    
    void changeByReference(int &a)
    {
        a = 5;
    }
    
    void changeByPointer(int *p)
    {
        *p = 8;
    }
    
    
    int main()
    {
        int val = 5;
        changeByReference(val);
        printf("%d
    ", val);
    
        int num = 12;
        changeByPointer(&num);
        printf("%d
    ", num);
    
        getchar();
        return 0;
    }
    使用引用的话,就能够像操作一个普通变量一样方便,上述代码中的引用a并没有带上*,而指针p在使用的时候,要带上个*p。

    使用引用的另一个优点是安全,C++的指针太强大了,一旦没用好就会造成非常多问题。而引用的功能则弱得多,在不须要那么强大功能的时候使用引用显得安全。

    如今回过头来看看使用引用怎样来申请内存:

    void getMemory(int *&p)
    {
        p = (int *)malloc(sizeof(int) * 10);
    }
    
    void func()
    {
        int *p = NULL;
        getMemory(p);
    }
    指针的引用就代表了指针自身,所以使用引用能正确申请到内存,这个应该没什么疑问。剩下的问题就是:指针的引用怎么表示?通过上述代码我们知道了是*&p。

    我们来看看*&p和&*p的差别。

    先来解析一下int类型的引用。由于引用的类型肯定是目标变量的类型,所以肯定是int,又由于规定&为引用符,所以int型的引用就非常好写了。*&p == *(&p),相同的,对于指针变量,其引用的类型肯定也是指针类型,所以上述代码中的引用变量肯定是int*类型的,指针是p,引用一下,就是&p。好了结果出来了,是int* &p,那不就是int *&p了。

    那么&*p又是什么呢?&*p == &(*p),*p是指针所指向的目标,&这个目标变量,那么结果就是"取指针指向的目标的地址",想想看,还有什么地方存了这个地址?当然是指针自身内存上的值了。如果p指针指向变量num,那么&*p、p、&num是等价的。知道了*和&的概念后,指针就能够任意玩转了:

    #include <iostream>
    using namespace std;
    
    int main()
    {
        int num = 888;
        int *p = &num;
        
        printf("%d %d %d
    ", *(*&p), *&num, *(&*p));
        printf("%x %x %x
    ", &num, &*p, *&p);
    
        getchar();
        return 0;
    }
    可能你会问,上面代码中的*&p和&*p都是p的意思,为什么申请内存的时候仅仅能写*&p而写了&*p就报错了呢?

    *&p是引用,引用是一种类型,而&*p是一种取地址的操作,不是类型。就好像int num = 5,在printf里面你能够写num也能够写5,可是在代码里你仅仅能写num而不能写5,由于num是一种类型而5不是。

  • 相关阅读:
    一道360 crackme的详细分析
    Unity 3d游戏逆向之.NET Reflector工具使用介绍
    如何获取系统Home(Launcher)应用判断用户是否处于home界面
    IDA Pro反编译代码类型转换参考
    Spring揭秘笔记
    Could not resolve archetype org.apache.maven.archetypes:maven-archetype-quick
    Oracle修改密码后,登录sys或者sysdba时一直提示:ORA-01017: invalid username/password; logon denied
    Springboot项目中调用Controller方法出现:java.lang.IllegalArgumentException: Unknown return value type: java.lang.Boolean
    在idea中导入Gradle项目的一些记录
    IDEA修改项目的JDK版本
  • 原文地址:https://www.cnblogs.com/hrhguanli/p/4052761.html
Copyright © 2020-2023  润新知