【转】值传递与引用传递

  看了几篇值传递和引用传递的好文章,特此汇总后转发出来,希望给迷途的小羔羊们一些帮助,分割线代表不同但是联系十分紧密的文章,有方法有实例,大家好好看看

C++中引用传递与指针传递以及值传递区别
 
从概念上讲。指针从本质上讲就是存放变量地址的一个变量,在逻辑上是独立的,它可以被改变,包括其所指向的地址的改变和其指向的地址中所存放的数据的改变。
 
而引用是一个别名,它在逻辑上不是独立的,它的存在具有依附性,所以引用必须在一开始就被初始化,而且其引用的对象在其整个生命周期中是不能被改变的(自始至终只能依赖于同一个变量)。
 
值,简单而言就是一个变量,
 
在C++中,值、指针和引用经常用于函数的参数传递,然而,值作为参数传递和指针传递参数于引用传递参数是有很大不同的,值传递简单而言就是把一个变量拷贝一份传递过去,在函数体内对这个值得改变不会影响原来的值,这里不多做讨论,指针和引用作为参数传递本质上的不同的:
 
指针传递参数本质上是值传递的方式,它所传递的是一个地址值。值传递过程中,被调函数的形式参数作为被调函数的局部变量处理,即在栈中开辟了内存空间以存放由主调函数放进来的实参的值,从而成为了实参的一个副本。值传递的特点是被调函数对形式参数的任何操作都是作为局部变量进行,不会影响主调函数的实参变量的值。(这里是在说实参指针本身的地址值改变不会影响原来的指针)
例1:
void getMemory(int *p)
{
p = new int[100];
void main()
{
int *p;
getMemory(p);
memcpy(p, "hello world", 30);
cout<<p<<endl;
}
这里的输出时错误的,因为对于实参而言实际是一个地址值,在getMemory中改变的只是他的一个副本,不会影响p本身,并且这个函数每调用一次就会泄露一块内存。
 
在引用传递过程中,被调函数的形式参数虽然也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。被调函数对形参的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量。正因为如此,被调函数对形参做的任何操作都影响了主调函数中的实参变量。
 
引用传递和指针传递是不同的,虽然它们都是在被调函数栈空间上的一个局部变量,但是任何对于引用参数的处理都会通过一个间接寻址的方式操作到主调函数中的相关变量。而对于指针传递的参数,如果改变被调函数中的指针地址,它将影响不到主调函数的相关变量(例1)。如果想通过指针参数传递来改变主调函数中的相关变量,那就得使用指向指针的指针,或者指针引用。
 
以下的概念比较重要
从语法的角度看,你只要把指针声明语句里的指针名字去掉,剩下的部分就是这个指针的类型如char*ptr;//指针的类型是char*
 
从语法上看,你只须把指针声明语句中的指针名字和名字左边的指针声明符*去掉,剩下的就是指针所指向的类型如char*ptr; //指针所指向的的类型是char
 
在32 位程序里,所有类型的指针的值都是一个32 位整数,因为32 位程序里内存地址全都是32 位长。指针所指向的内存区就是从指针的值所代表的那个内存地址开始,长度为sizeof(指针所指向的类型)的一片内存区以后,我们说一个指针的值是XX,就相当于说该指针指向了以XX 为首地址的一片内存区域;我们说一个指针指向了某块内存区域,就相当于说该指针的值是这块内存区域的首地址
 
 
为了进一步加深大家对指针和引用的区别,下面我从编译的角度来阐述它们之间的区别:
 
程序在编译时分别将指针和引用添加到符号表上,符号表上记录的是变量名及变量所对应地址。指针变量在符号表上对应的地址值为指针变量的地址值,而引用在符号表上对应的地址值为引用对象的地址值。符号表生成后就不会再改,因此指针可以改变其指向的对象(指针变量中的值可以改,而引用对象则不能修改。
 
最后,总结一下指针和引用的相同点和不同点:
 
★相同点:
 
●都是地址的概念;
 
指针指向一块内存,它的内容是所指内存的地址;而引用则是某块内存的别名。
 
★不同点:
 
●指针是一个实体,而引用仅是个别名;
 
●引用只能在定义时被初始化一次,之后不可变;指针可变;引用“从一而终”,指针可以“见异思迁”;
 
●引用没const,指针有const,const的指针不可变;(具体指没有int& const a这种形式,而const int& a是有的,  前者指引用本身即别名不可以改变,这是当然的,所以不需要这种形式,后者指引用所指的值不可以改变)
 
●引用不能为空,指针可以为空;
 
●“sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身的大小在32位系统下为4;
 
●指针和引用的自增(++)运算意义不一样,指针的自加时自加一个sizeof(变量类型)这么大的地址,也即是改变的指针的指向的地址,引用的++表示引用所指向的值得++,
例2
int main()
{
int x[100];
for (int i = 0; i < 100; i++)
{
x[i] = rand() % 100;
}
int y = 100;
int &m = y;
int *p = x;
cout <<m++<<" "<< m <<endl;
cout <<(p++)<<" "<< p <<endl;
system("pause");
}
输出:
100     101
0017FCCC        0017FCD0
 
●引用是类型安全的,而指针不是(引用比指针多了类型检查)
 
 
 使用引用的好处:
1、传递可变参数,可作为函数参数传递可在函数内部修改,返回结果为修改后的结果,实现一个函数可以类似多个返回。
 
2、给函数传递大型对象
当大型对象被传递给函数时,使用引用参数可使参数传递效率得到提高,因为引用并不产生对象的副本,也就是参数传递时,对象无须复制。
 
******************************************************************************

一直分不清楚他们是干什么的,这回好好的总结下:

1:按值传递:

    程序如下:

[cpp] view plaincopy
 
  1. #include <iostream>  
  2. using namespace std;  
  3. void swap(int a,int b)//指针保存地址  
  4. {  
  5. int c;  
  6. cout<<"前: a:"<<a<<"  b:"<<b<<endl;  
  7. c=a;  
  8. a=b;  
  9. b=c;  
  10. cout<<"后: a:"<<a<<"  b:"<<b<<endl;  
  11. }  
  12. int main()  
  13. {  
  14.     int a=3,b=4;  
  15.     cout<<"主程序前: a:"<<a<<"  b:"<<b<<endl;  
  16.     swap(a,b);//取地址传递 变量的内存地址  
  17.     cout<<"主程序后: a:"<<a<<"  b:"<<b<<endl;  
  18.     return 0;  
  19. }  

输出的结果是:

主程序前: a:3  b:4

前: a:3  b:4

后: a:4  b:3

主程序前: a:3  b:4

主程序的值没有改变,改变的值是栈中的a 和b的副本的值,值传递的时候在栈中拷贝一个变量的副本来操作,这样的话就改变不了以前的值.用的时候要小心.

       

2:按址传递:

这个时候是用地址传递 程序如下:

 

[cpp] view plaincopy
 
  1. #include <iostream>  
  2. using namespace std;  
  3. void swap(int *a,int *b)//指针保存地址  
  4. {  
  5. int c;  
  6. cout<<"前: a:"<<*a<<"  b:"<<*b<<endl;  
  7. c=*a;  
  8. *a=*b;  
  9. *b=c;  
  10. cout<<"后: a:"<<*a<<"  b:"<<*b<<endl;  
  11. }  
  12. int main()  
  13. {  
  14.     int a=3,b=4;  
  15.     cout<<"主程序前: a:"<<a<<"  b:"<<b<<endl;  
  16.     swap(&a,&b);//取地址传递 变量的内存地址  
  17.     cout<<"主程序后: a:"<<a<<"  b:"<<b<<endl;  
  18.     return 0;  
  19. }  

这个就是按地址传递 在方法swap(&a,&b);的参数中&a ,&b都是取地址 那么传递过去的地址由指针来接收 所以在定义函数的时候有void swap(int *a,int *b)定义了俩个指针来存放地址,这样就好理解了 swap(){}函数里面的操作都是按指针来操作的*a ,*b

这样的话结果就是:

主程序前: a:3  b:4

前: a:3  b:4

后: a:4  b:3

主程序前: a:4  b:3

可以看到结果是改变了,因为直接操作的是a,b的内存地址

3:按别名(引用)传递:

[c-sharp] view plaincopy
 
  1. #include <iostream>  
  2. using namespace std;  
  3. void swap(int &a,int &b)//接收俩个别名  
  4. {  
  5. int c;  
  6. cout<<"前: a:"<<a<<"  b:"<<b<<endl;  
  7. c=a;  
  8. a=b;  
  9. b=c;  
  10. cout<<"后: a:"<<a<<"  b:"<<b<<endl;  
  11. }  
  12. int main()  
  13. {  
  14.     int a=3,b=4;  
  15.     cout<<"主程序前: a:"<<a<<"  b:"<<b<<endl;  
  16.     swap(a,b);//直接传递a b 传递过去的时候由别名接收 那么就是自身的别名了  
  17.     cout<<"主程序后: a:"<<a<<"  b:"<<b<<endl;  
  18.     return 0;  
  19. }  
  

  这样的话 接受的参数就是俩个别名,他们分别是主函数中a b的别名,因为别名和自身一样 所以结果就改变了

主程序前: a:3  b:4

前: a:3  b:4

后: a:4  b:3

主程序前: a:4  b:3

可以看到用值传递一不小心就达不到目的了,用地址传递的话写法会比较困难,不小心会出错,用引用传递的话考虑的问题就没有那么多了,我们就可以放心的使用了,写函数的时候用引用,那么调用的时候就不用考虑太多了.

**********************************************

C++中按值传递对象时,会调用拷贝构造函数生成对象的副本,那么对应的C#中也是这样的吗?

无论是在C++中还是在C#中,当变量或对象作为函数参数进行传递时都有两种方式:按值传递和按引用传递。

所谓按值传递是指在函数体内部使用的是对象的副本,在C++中这个副本是调用对象的拷贝构造函数完成的,而函数对副本的修改不会影响原来的对象。如

//C++

void Fun1(Student ss)

{

    ... ...  //ss进行处理和修改――实际处理的是传入对象的副本

}

 ... ...

 Student s7 ;

 Fun1(s7) ;//此函数调用结束后,对象s7的状态并没有改变

... ...

所谓按引用传递是指传给函数的实际上是对象的地址,这样函数对对象的修改就会反应在对象中,使对象的状态发生变化。如

//C++

void Fun2(Student& ss)

{

    ... ...  //ss进行处理和修改

}

... ...

Student s8 ;

Fun2(s8) ;//此函数调用结束后,对象s8的状态发生改变

... ...

C++中,可以通过指针和“&”引用符实现引用传递。上面的例子用到了“&”引用符号,其实换成指针也可以达到同样的效果。如果我们再进一步去想,可以发现,当用指针进行引用传递时,也发生了复制,只不过复制的是指针的值(即对象的地址),而不是复制指针指向的对象。这可以通过如下例子得到证明。

//C++

void Fun3(Student* ss)

{

    ... ...  //ss指向的对象进行处理和修改

    ss = NULL ;

}

... ...

Student* s9 ;

Fun3(s9) ;//此函数调用结束后, s9指向的对象的状态发生了改变

... ...   

但是在Fun3s9)调用完后,s9并不是NULL ,这说明Fun3中使用的是指针s9的副本。如果再进一步,我们可以猜测用“&”符实现引用传递时也发生了同样的故事。事实上也是这样,C++中的引用只是一个受限却更加安全的指针而已。

那么按引用传递和按值传递各有什么好处了?

按引用传递不需要发生拷贝行为,因此速度快,特别是大对象时,这种优势很明显。按值传递时对传入对象的修改实际是对对象副本的修改,不会影响原对象的状态。

你也许会想到如果采用const引用传递那么就可以得到双倍的好处,可以这么说,但是不要走极端。

一般而言,将不允许改变的大对象作为const引用传递给函数是很合适的,但如果是简单类型或自定义的小对象直接用值传递就可以了。

 

如果外界一定要看到函数对对象的修改,那么只有一条路 ―― 按引用传递。

C#中情况却发生了变化,C#中的引用类型的对象都是按引用传递且只能按引用传递。而值类型对象(或者称为变量),通常情况下是按值传递的。如果要按引用传递值类型对象,那么就要使用关键字refout refout的唯一区别是ref用修饰参数时要求传入的变量被初始化过。

由于类是引用类型,而所有的引用类型的对象的传递都是引用传递,所以在此过程中根本不会发生拷贝函数的调用。照这样看来,根本就没有必要有拷贝构造函数了。

我想现在你已经知道了C# 中为什么不需要拷贝构造函数和很少调用赋值运算符了。你也许会问既然是很少调用赋值运算符,那一定还有调用赋值运算符的情况存在,那么这种情况是怎样的?那是因为类的相仿体――结构struct 

原文地址:https://www.cnblogs.com/yican/p/3304988.html