【1】、C++中函数的参数传递包括:值传递、指针传递、引用传递这三种方法。

         注:这里的值是指实体的内容值;指针是指实体的地址;引用是指实体的别名(本质是受限制的指针)。但无论是数值还是指针都是当作整型处理的,即值。

 

1、c/c++中参数传递默认都是传值:结果,形参和实参分属两个不同地址空间内,形参得到的是实参的值的拷贝。改变形参不会影响实参。

         例,

     

void swap(int p,int q)
         {
             int temp;
             temp=p;
             p=q;
             q=temp;
         }

         这个函数并不会真的交换实参的值。

 

         例,

      

vector findVi(vector<vector>  vvi)
         {
                  for (int i = 0; i < vot.size(); i++) {
                          for (int j = 0; j < vvi[i].size(); j++) {
                                   if (vvi[i][j] == -1)
                                   {
                                            return vot[i];
                                   }
                          }
                  }
         }

         这个函数的目的是返回矩阵中含值为-1的向量。采用的是默认的值传递方式,会在栈中另行开辟一个临时的向量组空间进行操作,虽然可以获得正确的结果,但效率太低。而且返回值也是值类型不好(后面有说),应该定义为                      vector& findVi(vector<vector>&  vvi)。

 

 

2、指针传递:传递给形参指针变量的是地址,也就是形参指向实参的内存空间,这样就可以通过指针直接对实参操作。形参的操作结果也就可以改变实参的值。

         例,

    

void swap(int *p,int *q)
         {
             int temp;
             temp=*p;
             *p=*q;
             *q=temp;
         }

         可以正确交换实参的值,而且节省资源,效率高。

         特别的,

     

void swap3(int *p,int *q)
         {
             int *temp;
             temp=p;
             p=q;
             q=temp;
         }

         这个函数虽然也是传地址,但交换的是指针,指针空间的值并没有交换。

 

 

3、引用传递:形参引用就是实参变量的一个别名。对引用的操作自然就是对实参的操作,比起指针更简洁、安全。

       

void swap(int &p,int &q)
         {
             int temp;
             temp=p;
             p=q;
             q=temp;
         }

 

 

        

特别的:数组做形参,也作为指针传递,但这是一个常量指针。即能用它做指针运算然后操作实参数组元素,但不能对它重新赋值。

      

void printValues(int *a){ }
         void printValues(int a[]){ }
         void printValues(int a[10]){ }  //系统都是把做形参的数组当常量指针处理的,这里的10并没有作用。

 

特别的:函数名做形参也是指针传递,一个常量函数指针。

 

 

 

【2】、函数返回值:根据返回的形式分返回值、返回地址、返回引用。根据被返回的实体的内存位置分:全局区、栈区、堆区。

        

         拿下面这个函数为例,目的是返回矩阵中含值为-1的向量。

         返回值:

      

vector findVi(const vector<vector>&  vvi)
         {
                  for (int i = 0; i < vot.size(); i++) {
                          for (int j = 0; j < vvi[i].size(); j++) {
                                   if (vvi[i][j] == -1)
                                   {
                                            return vot[i];
                                   }
                          }
                  }
         }

         返回地址:

        

vector* findVi(const vector<vector>&  vvi)
         {
                  for (int i = 0; i < vot.size(); i++) {
                          for (int j = 0; j < vvi[i].size(); j++) {
                                   if (vvi[i][j] == -1)
                                   {
                                            return &vot[i];
                                   }
                          }
                  }
         }

         返回引用:

       

vector& findVi(const vector<vector>&  vvi)
         {
                  for (int i = 0; i < vot.size(); i++) {
                          for (int j = 0; j < vvi[i].size(); j++) {
                                   if (vvi[i][j] == -1)
                                   {
                                            return vot[i];
                                   }
                          }
                  }
         }

 

         首先应当注意的是形参是用传递引用来操作地址空间的,虽然省时省力,但在这里的过程中不应该改变原实参,故加const修饰。

         第一种传值,主函数可以这样调用vector<int> vi = findVi(vvi);(vvi这个矩阵在这之前自然是存在的)。这样的话,findVi这个函数是返回值给vi这个向量。即将矩阵中符合条件的向量值拷贝给向量vi,对vi来说就是在定义的同时用另一个向量初始化它。这就是说传值是要占用资源的,尤其是这个值的数据量很大的时候,这是传值的弊端。好处是这个获得的值可以一直保存到超出vi的生命期,而在这过程中不论vvi这个矩阵怎么变。

         第二、三种传地址和引用,主函数可以这样调用vector<int>* vi = findVi(vvi);或vector<int>& vi = findVi(vvi)。获得的是指向符合条件向量的指针或引用。这样若是要输出这个向量,直接根据这个返回的指针或引用到vvi这个矩阵中读取就好了,没有额外资源开销。但这是有时效性的,一旦vvi向量做出改变或者被释放了,后面要再用vi这个指针或引用去读取值势必会出问题。

 

         上面三种返回类型的值都是在栈中的,看下面情况:

        

       

int* doTest()
         {
                  int a = 3;
                  return &a;
         }

         这个返回没问题,而且cout<<*doTest();还能输出3。但这是非法的,a是个局部变量,申请在栈上,当这个函数调用结束返回后,这个变量所在的空间就归还给系统了。之所以还能输出3,其一是函数调用结束后使用的栈空间并没有打扫(还记得局部变量未经初始化,其值是不确定的?),其二指针是把双刃剑,只要有地址,不论合理与否都能搞事。从函数中返回局部变量的值没问题,其实质是返回的局部变量值的一个拷贝,但返回局部变量的指针就有问题了,指不定就会出事。

 

 

char* doTest()
         {
                  char* s = "235434634sfd";
                  return s;
         }

         这个返回没问题,但这是只读的,若想更改就会出错(delete[]也会出错),因为这个返回地址是在全局数据区的,而且是常量值。

 

       

char* doTest()
         {
                  static char s[] = "235434634sfd";
                  return s;
         }

         这个返回值没问题,可读也可写,返回地址是在全局数据区的,是静态局部变量。与上面相比,上面不能写是因为,s是直接指向“235434634sfd”这个在全局数据区的常量。而下面,s是保存在全局数据区的变量,其初始值是串“235434634sfd”常量的一份拷贝。

 

 

       

string* doTest()
         {
                  string* s = new string("abc");
                  return s;
         }

         这个返回的是堆区指针,没问题。但记得在主函数中delete[]。

 

 

上面用的一些简单数据类型,很多操作都是用的系统默认的,下面通过自定义一个Test类来仔细分析函数传值的过程:

class Test
{
public:
         Test() {}
         Test(const Test& t)
         {
                  cout << "复制构造"<<endl;
         }
         Test& operator= (const Test& t)
         {
                  cout << "赋值运算符函数" << endl;
                  return *this;
         }
         ~Test()
         {
                  cout << "析构" << endl;
         }
};
 
Test fun(Test u)
{
         Test t = u;
         return t;
}
 
int main()
{
         Test x, y;
         x = fun(y);
 
         getchar();
}

输出结果为:

 

分析:(1)当调用函数fun,并以传值方式传参时,用y对象拷贝构造出u,故会调用复制构造函数并输出“复制构造”。

         (2)函数体中用u拷贝构造出t,故又调用复制构造函数并输出“复制构造”。

         (3)因为函数返回时,以返回值的方式,故再次调用复制构造函数并输出“复制构造”。这时产生的是一个临时对象。

(4)u,t是函数fun内的局部变量,当函数返回时生命期结束,因为是创建在栈上,故会自动调用析构函数。析构的顺序为出栈的顺序(入栈相反),故第一个“析构”是t调用析构函数时输出的,第二个是u析构时输出的。

(5)当函数调用结束返回时,会带回(3)的临时对象。因为是在一个赋值表达式中,故用这个临时对象给x重新赋值,这时调用赋值运算符函数会输出“赋值运算符函数”。

(6)当函数调用并赋值的语句结束,(3)产生的临时对象才超出生命期,这个临时对象也是在栈上,故自动调用析构函数输出“析构”。

 

反思:若把上面的mian函数稍做改动呢?

int main()
{
         Test y;
         Test x = fun(y);
 
         getchar();
}

 

 

总结:c++的函数是程序的基本组成单位。它不仅仅是数学上定义域到值域的映射,更是灵活多样的过程。函数传递的是值,那么这个函数过程主要是在自己的栈空间内进行的。但麻烦在于因为传值可能产生大量数据的拷贝,而且拷贝过去了还得占用一定栈空间,效率确实低。若函数传递的是地址和引用,那么这个灵活多样的过程就赋予了操作“异地”数据的权力,也就是可以直接通过指针或引用操作实参了,避免了大块数据的挪动。但这个过程中对实参的写入可能不是自己希望的(如果只是读记得const),更麻烦的是后面,引用还好点有所限制,指针就跳脱了,只要知道一个直接地址,再指定一个访问类型,就可以搞事了。函数传参类型的选择伴随着效率跟可能的风险,尤其指针是把双刃剑,应当慎重使用,其实在大多数函数传参时可以传引用,安全又简洁。

         再说函数的返回值,类型上有值、指针、引用,位置上有全局区、栈区、堆区。这更是得慎重了。总的来说,返回值都是可以的,因为最终都是返回给调用函数一份值的拷贝,但数据量大了效率不好。再说指针和引用,不是什么都能返回的,栈区的本函数内局部变量是绝对搞事的,栈区的本函数外局部变量倒是可以,但记得有时效性。至于静态局部变量,它的作用域本身就是在函数内的,强行返回指针到函数外干嘛?就算是返回了也得掂量是否就能写入。堆区就没啥好说的了,记得有借有还就好。

         话说c++中函数的传参、返回值真够让人提心吊胆的!仅这么一项就烧脑袋了。还是java这种专注面向对象的来得简洁,除了基本类型外其他引用数据类型全部自己就传递引用,而且这个引用的传递和操作还自己伴随着安全性等诸多检查,开发者专注于程序功能就好了,不过在这种层层的面向对象“外衣”和各种机制下其效率确实差c/c++多了。当然前提是在c++这种可以有多种程序设计方法:面向过程的低级编程、面向对象高级编程下构建“合理”的程序。要是写出一个程序即没有低级过程的高效率,又凸显不了高级编程的可维护性、可扩展性,那得多尴尬???