一说到指针和函数的关系,很多人就会想到指针作为函数的参数。但是,可能很少有人会注意指针作为函数的参数时的真正意义。也许我们自己的程序很少用指针作为函数的返回值,但是,它确实存在,而且意义非凡。这部分将介绍指针和函数之间很少被人注意的关系。
5.1 函数的参数
函数的参数的传递方式无外乎两种,一是传值一是传址。一般的教课书上都认为数组和指针都是传址调用,而只有普通的数据类型(如整型、结构体等)才是传值调用。但,我并不这么认为。
我认为只有显示调用传值调用符号&的变量才是传址调用,其他的都是传值调用。对于指针和数组来说传的值是指针的值/数组首元素的地址,此时,再利用指针/数组去访问元素则是相当于操作原先元素的地址了,效果和把原先元素的地址传递过来是一样的,但在形参和实参结合的时候确实进行了一次赋值,而这个值是指针的值,并且指针的值又是原先元素的地址。
变量都有其地址,机器都是通过其地址引用变量的。传值是指将实参的值赋给形参,而形参和实参的地址显然是不同的,他们的作用域也不同,形参只作用于该函数,并存储于堆栈中,在堆栈中占有一个固定的地址。在形参和实参匹配时,会把堆栈中这个地址的内容修改成实参的值,这就是传值。
传址则是指在形参和实参匹配时,将被调用函数中所有形参的代号全部修改成调用函数中实参的地址。这时,被调用函数中访问该形参就是访问该函数空间外的某个地址。
由于在函数的参数中,数组总是被解释为指针,我们只要分析下指针作为函数参数的情形就行了。
int test(int *p){.....};
int main()
{
int a[] = {1,2,3};
int *p = a;
test(p);
……
}
此时,在test函数的栈中,为形参p预留了一个4字节的空间,并且符号p就是这个空间的首址。在函数形参和实参匹配时,形参p的值就变成了实参p的值,此时发生了一次赋值,这个赋值的过程就是传值。但碰巧的是,这个值正好是数组a第一个元素的地址,所以后面的*(p+i)之类的操作,就是修改a数组了。某种意义上,这种传值方式对于a来说是传址调用,但此处的形参p和实参p的地址绝对是不一样的,此时修改形参p的值,绝对不会影响实参p的值,只是修改形参p所指向的空间才会影响实参p所指向的空间。
测试用例:
#include <iostream>
using namespace std;
#include <strings.h>
struct Node
{
int a;
int b;
};
int test(int a,int b[],int *c,int &d,Node ab,Node &abc)
{
cout<<&a<<" "<<a<<endl;
cout<<(unsigned int)&b<<" "<<(unsigned int)b<<endl;
cout<<&c<<" "<<*c<<endl;
cout<<&d<<" "<<d<<endl;
cout<<(unsigned int)&ab<<endl;
cout<<(unsigned int)&abc<<endl;
return 1;
}
int main()
{
int a =1;
int b[] = {2,3,4};
int f = 5;
int *c =&f;
int d =6;
Node ab;
Node abc;
cout<<&a<<" "<<a<<endl;
cout<<(unsigned int)&b<<" "<<(unsigned int)b<<endl;
cout<<&c<<" "<<*c<<endl;
cout<<&d<<" "<<d<<endl;
cout<<(unsigned int)&ab<<endl;
cout<<(unsigned int)&abc<<endl;
test(a,b,c,d,ab,abc);
return 0;
}
结果:
0xbff4a19c 1
3220480372 3220480372
0xbff4a194 5
0xbff4a190 6
3220480392
3220480384
0xbff4a150 1
3220480340 3220480372
0xbff4a158 5
0xbff4a190 6
3220480352
3220480384
从上述结果,我们可以看到,只有在形参前面加了传址符号&后,形参和实参的地址才是一样的,此时只是将被调用函数中形参符号全部改成调用函数中的地址。对于数组和指针的方式只是指针的传值调用罢了,指针的地址是不一致的。
注:当结构体作为函数的参数,并且结构体中有指针成员时要特别注意。在结构体作为函数的参数时,形参—实参匹配的过程虽然是个传值的过程,即把实参结构体中的所有成员的值赋给形参结构体中相应的成员。按理说形参和实参中所有的成员都有自己的地址是没有联系的,但是不要忘了对于指针成员来说虽然它们都有自己的存储地址,但是他们所指向的空间是一致的。这点,同样适用于结构体赋值。即=默认重载赋值方式。
5.2 指针作为函数的返回值
函数的返回值不能是数组也不能是函数,但函数的返回值可以是指针,而这个指针可以是指向数组的指针,也可以是指向函数的指针。由于指针可以指向任何由地址表示的东西,所以,某种意义上函数的返回值可以任何东西,我们需要做的仅仅是用个指针指向它而已。只是当函数的返回值是指针的情况下要注意指针指向的空间在函数返回时不能释放。为此,我们不能用局部变量的地址作为指针的值,再将该指针返回。我们可以用以下的方法代替:
方法1:用常量区作为返回值。(只适用于字符串常量)
char* test(){return "const data";}
由于字符串常量存储于常量区,而常量区里的内容不会随着该函数的返回而失效。
方法2:用全局变量。
全局变量不利于数据的封装,而且全局变量过多将会使程序更加混乱。但对于小程序,全局变量不失为一种简单有效的方法。
方法3:用静态变量。
静态变量虽然在函数返回时并不释放,但是它同全局变量一样,再次调用此函数会影响前面的结果,使这类函数成为线程不安全型。
方法4:动态开辟空间。
这种方法是相对比较常用的,但是需要调用者在使用后主动释放内存,同时,并不是所有申请内存都能成功,并且多次申请和释放内存后会引起大量的内存碎片降低系统性能。
方法5:放弃用返回值传出大量信息,改用函数的参数。
利用这种方法,我们也可以让函数返回指向什么东西的指针,但此时这个返回值就是指向该函数的某个实参(不是形参)的指针,那么对于调用函数的参数来说,这个返回值就是可有可无的了。
函数的返回值的情况多种多样,本文着重讨论比较不常见的情况,但为了便于理解,对于常见的情况也简要涉及。现简要说明下函数返回值的各种情况。
情况1:一般数据类型作为函数返回值。
例子:int test();
情况2:结构体作为函数的返回值。
例子:Node test();
情况3:无返回值
例子:void test();
情况4:一般数据类型指针作为函数返回值。
例子:int* test();
情况5:结构体指针作为函数返回值。
例子:Node * test();
情况6:万能指针作为函数返回值。
例子:void* test();
情况7:指向函数的指针作为函数的返回值。
例子:void (*test())(); 这个函数的返回值是个指向无返回值的无参函数的指针。
情况8:用指向实参的指针作为返回值。
例子:Node* test(Node &Var){return &Var;}
Node* test(Node* pVar){return pVar;}
备注:
1.由于函数名即为函数的地址,而且函数属于代码段在整个程序的运行中一直有效,所以对于返回值是指向函数的指针,只要返回该函数的名字就行了。
2.int test(); int (*ptest)(); ptest = test; ptest = &test;则,有以下几种方法访问test函数。
test(); ptest(); (*ptest)();
3.函数名在使用时总是会被编译器转换为函数的指针,所以在赋值时我们可以把函数名直接赋值给函数指针即用ptest = &test;代替ptest = test。同理,我们也可以用ptest();代替(*ptest)();
4.用形参数指针作为其返回值是不正确的,因为形参存储于该函数的堆栈中,函数返回后就销毁了。
例如Node* test(Node pVar){return &pVar;}是不正确的。